<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: CodeCraft Diary</title>
    <description>The latest articles on Forem by CodeCraft Diary (@codecraft_diary_3d13677fb).</description>
    <link>https://forem.com/codecraft_diary_3d13677fb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3572568%2F9b328bfe-d229-4c84-8915-9af499c7bff0.png</url>
      <title>Forem: CodeCraft Diary</title>
      <link>https://forem.com/codecraft_diary_3d13677fb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/codecraft_diary_3d13677fb"/>
    <language>en</language>
    <item>
      <title>Laravel Testing: RefreshDatabase vs DatabaseTransactions (What Actually Matters)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 31 Mar 2026 13:00:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/laravel-testing-refreshdatabase-vs-databasetransactions-what-actually-matters-4fje</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/laravel-testing-refreshdatabase-vs-databasetransactions-what-actually-matters-4fje</guid>
      <description>&lt;p&gt;Laravel RefreshDatabase vs DatabaseTransactions is one of the most common sources of confusion when writing tests in Laravel.&lt;/p&gt;

&lt;p&gt;Choosing the wrong approach can lead to flaky tests, hidden bugs, and unreliable results.&lt;/p&gt;

&lt;p&gt;When writing tests in Laravel, database state can quickly become a source of confusion.&lt;/p&gt;

&lt;p&gt;One test passes, another fails. Data seems to “leak” between tests. Records appear when they shouldn’t — or disappear when you expect them to exist.&lt;/p&gt;

&lt;p&gt;If you’ve experienced this, you’re not alone.&lt;/p&gt;

&lt;p&gt;In most cases, the issue comes down to how your tests handle the database. Laravel provides two primary approaches for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RefreshDatabase
&lt;/li&gt;
&lt;li&gt;DatabaseTransactions
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first glance, they seem similar. In reality, they behave very differently — and choosing the wrong one can lead to subtle bugs or false confidence in your test suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding the Core Problem
&lt;/h2&gt;

&lt;p&gt;Tests must be isolated.&lt;/p&gt;

&lt;p&gt;Each test should run independently, without being affected by previous tests.&lt;/p&gt;

&lt;p&gt;If your database is not reset properly between tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;data can persist unexpectedly
&lt;/li&gt;
&lt;li&gt;test results become unreliable
&lt;/li&gt;
&lt;li&gt;debugging becomes painful
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Laravel solves this problem using two strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reset the database completely
&lt;/li&gt;
&lt;li&gt;Wrap each test in a transaction and roll it back
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Option 1: RefreshDatabase
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Foundation\Testing\RefreshDatabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;RefreshDatabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs migrations before tests&lt;/li&gt;
&lt;li&gt;Ensures a clean database state&lt;/li&gt;
&lt;li&gt;Uses transactions internally when possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Advantages&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High reliability&lt;/li&gt;
&lt;li&gt;Works with queues, jobs, events&lt;/li&gt;
&lt;li&gt;Predictable behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slower&lt;/li&gt;
&lt;li&gt;Heavier setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Option 2: DatabaseTransactions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Foundation\Testing\DatabaseTransactions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;DatabaseTransactions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts a database transaction before each test&lt;/li&gt;
&lt;li&gt;Rolls it back after the test finishes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Advantages&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast&lt;/li&gt;
&lt;li&gt;Lightweight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages&lt;/strong&gt;&lt;br&gt;
Does NOT work well with queues or async processes&lt;br&gt;
Can lead to hidden inconsistencies&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use RefreshDatabase when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;testing queues or jobs&lt;/li&gt;
&lt;li&gt;working with multiple DB connections&lt;/li&gt;
&lt;li&gt;you need maximum reliability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use DatabaseTransactions when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;testing simple DB logic&lt;/li&gt;
&lt;li&gt;performance is critical&lt;/li&gt;
&lt;li&gt;everything runs in one request lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Things Break in Real Projects
&lt;/h2&gt;

&lt;p&gt;This is where most developers run into problems.&lt;/p&gt;

&lt;p&gt;Especially when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;jobs don’t see database data&lt;/li&gt;
&lt;li&gt;tests pass locally but fail in CI&lt;/li&gt;
&lt;li&gt;retries cause unexpected behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;-&amp;gt;&lt;/strong&gt; I go deeper into real-world failures, debugging strategies, and edge cases here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-&amp;gt;&lt;/strong&gt; &lt;a href="https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Use RefreshDatabase for reliability&lt;br&gt;
Use DatabaseTransactions for speed&lt;br&gt;
Avoid mixing both&lt;br&gt;
Be careful with queues and async behavior&lt;/p&gt;

&lt;p&gt;If you're working with Laravel testing, you might also find useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue testing pitfalls&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;-&amp;gt;&lt;/strong&gt; &lt;a href="https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Feature testing in PHP&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;-&amp;gt;&lt;/strong&gt; &lt;a href="https://codecraftdiary.com/2025/10/30/feature-testing-in-php-ensuring-the-whole-system-works-together/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2025/10/30/feature-testing-in-php-ensuring-the-whole-system-works-together/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>laravel</category>
      <category>php</category>
      <category>testing</category>
    </item>
    <item>
      <title>AI-Driven Refactoring in PHP: When to Trust Copilot (and When to Take the Wheel)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 24 Mar 2026 14:00:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/ai-driven-refactoring-in-php-when-to-trust-copilot-and-when-to-take-the-wheel-405b</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/ai-driven-refactoring-in-php-when-to-trust-copilot-and-when-to-take-the-wheel-405b</guid>
      <description>&lt;p&gt;In 2026, the biggest performance gains in PHP don’t come from micro-optimizations—they come from I/O strategy.&lt;/p&gt;

&lt;p&gt;Legacy PHP codebases (5–10 years old) are overwhelmingly synchronous and blocking. Refactoring them toward concurrency—using Fibers with an event loop like Revolt, Amp, or ReactPHP—can unlock massive gains.&lt;/p&gt;

&lt;p&gt;But this is also where AI tools like Copilot become dangerous.&lt;/p&gt;

&lt;p&gt;They can accelerate the refactor—or quietly corrupt your architecture.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Prompt–Refactor–Verify” Cycle
&lt;/h2&gt;

&lt;p&gt;When working with AI, the difference between success and subtle bugs is prompt precision.&lt;/p&gt;

&lt;p&gt;Bad prompt:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Make this async.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Good prompt:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Identify I/O-bound operations. Refactor using Fibers with an event loop (Revolt/Amp). Avoid shared mutable state. Ensure deterministic completion.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you paste a 500-line method without context, AI will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;miss hidden dependencies&lt;/li&gt;
&lt;li&gt;ignore state coupling&lt;/li&gt;
&lt;li&gt;introduce non-obvious bugs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI is a transformer, not a system thinker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example: Legacy Image Processor
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (typical legacy flow)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// blocking&lt;/span&gt;
    &lt;span class="nv"&gt;$resized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// CPU&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$resized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// blocking&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. Predictable. Slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After (controlled concurrency with Amp)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Amp\Future&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;Amp\async&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$tasks&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// non-blocking if using async client&lt;/span&gt;
        &lt;span class="nv"&gt;$resized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$resized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Future\awaitAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What changed?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I/O is now concurrent&lt;/li&gt;
&lt;li&gt;Execution is interleaved, not parallel threads&lt;/li&gt;
&lt;li&gt;The bottleneck shifts from waiting → throughput&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The AI Trap: “It Works” ≠ “It’s Safe”
&lt;/h2&gt;

&lt;p&gt;AI often produces code that looks correct and even passes tests.&lt;/p&gt;

&lt;p&gt;But there are hidden risks.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Shared State Issues&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is dangerous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;processedUrls&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not because PHP is multithreaded—but because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;execution is interleaved&lt;/li&gt;
&lt;li&gt;order is no longer deterministic&lt;/li&gt;
&lt;li&gt;side effects accumulate unpredictably&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;return values → aggregate later&lt;/li&gt;
&lt;li&gt;avoid mutation inside async tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. The “AI Echo Chamber”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the most dangerous patterns:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI writes a bug → AI writes a test → test passes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bug: returns null on failure&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI-generated test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is green. Everything is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. “Happy Path” Bias&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI prefers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production systems, this is data loss.&lt;/p&gt;

&lt;p&gt;Senior-level expectation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit exceptions&lt;/li&gt;
&lt;li&gt;domain-level error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preventing Architecture Drift
&lt;/h2&gt;

&lt;p&gt;The biggest long-term risk isn’t bugs.&lt;/p&gt;

&lt;p&gt;It’s &lt;strong&gt;inconsistency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You refactor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Class A on Monday&lt;/li&gt;
&lt;li&gt;Class B on Tuesday&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By Friday:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;patterns diverge&lt;/li&gt;
&lt;li&gt;abstractions don’t align&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because AI only sees &lt;strong&gt;the current file&lt;/strong&gt;, not your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Constraint Header” Strategy
&lt;/h2&gt;

&lt;p&gt;Before any refactor, define rules:&lt;/p&gt;

&lt;p&gt;Context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strict types only&lt;/li&gt;
&lt;li&gt;No direct DB calls (Repository pattern)&lt;/li&gt;
&lt;li&gt;Async only via Amp&lt;/li&gt;
&lt;li&gt;Immutable DTOs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Task:&lt;br&gt;
Refactor InvoiceGenerator&lt;/p&gt;

&lt;p&gt;This reduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;random design decisions&lt;/li&gt;
&lt;li&gt;pattern drift&lt;/li&gt;
&lt;li&gt;“AI creativity” in critical code&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Killing the “God Constructor”
&lt;/h2&gt;

&lt;p&gt;Legacy PHP often looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;Logger&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;Mailer&lt;/span&gt; &lt;span class="nv"&gt;$mailer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;Cache&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;PaymentGateway&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI’s default move:&lt;br&gt;
→ add another dependency&lt;/p&gt;

&lt;p&gt;Wrong direction.&lt;/p&gt;
&lt;h2&gt;
  
  
  Refactoring toward composition
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderProcessor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;PaymentGateway&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;Notifier&lt;/span&gt; &lt;span class="nv"&gt;$notifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Pending&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AlreadyPaidException&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\UnhandledMatchError&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Order &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; processed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Key idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fewer responsibilities per class&lt;/li&gt;
&lt;li&gt;clearer boundaries&lt;/li&gt;
&lt;li&gt;easier testing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Hallucinated APIs: The “Ghost Methods” Problem
&lt;/h2&gt;

&lt;p&gt;AI sometimes invents functions that sound correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;array_first_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ❌ does not exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you don’t recognize it, verify it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Which PHP version introduced this?”&lt;/li&gt;
&lt;li&gt;“Show official docs”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No answer → hallucination.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost: AI-Grown Code
&lt;/h2&gt;

&lt;p&gt;If AI writes most of your refactoring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code becomes harder to reason about&lt;/li&gt;
&lt;li&gt;intent disappears&lt;/li&gt;
&lt;li&gt;onboarding slows down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Countermeasure:&lt;/p&gt;

&lt;p&gt;Ask AI:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Explain why this design was chosen over a simpler alternative.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the answer is vague → the design is weak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing AI Code Like a Senior
&lt;/h2&gt;

&lt;p&gt;Static analysis won’t save you.&lt;/p&gt;

&lt;p&gt;Tools like PHPStan check correctness—not intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually review
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Semantic correctness&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Did we simplify—or just move complexity?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Concurrency safety&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hidden state mutation&lt;/li&gt;
&lt;li&gt;ordering assumptions&lt;/li&gt;
&lt;li&gt;error propagation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask AI explicitly:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Simulate a timeout during async execution. What breaks?”&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 Refactoring Checklist
&lt;/h2&gt;

&lt;p&gt;Before merging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type safety → strict, explicit&lt;/li&gt;
&lt;li&gt;No hidden shared state&lt;/li&gt;
&lt;li&gt;Async is intentional (not accidental)&lt;/li&gt;
&lt;li&gt;Dependencies are minimal and meaningful&lt;/li&gt;
&lt;li&gt;Errors are explicit (no silent nulls)&lt;/li&gt;
&lt;li&gt;You can explain every change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If not:&lt;/p&gt;

&lt;p&gt;→ don’t merge it&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;AI is an accelerator—but also a multiplier of mistakes.&lt;/p&gt;

&lt;p&gt;It tends to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add complexity&lt;/li&gt;
&lt;li&gt;over-engineer&lt;/li&gt;
&lt;li&gt;hide intent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your role is no longer just writing code.&lt;/p&gt;

&lt;p&gt;It’s &lt;strong&gt;curating it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The real senior skill in 2026 is knowing when to say:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“This is too clever. Make it simple.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because great refactoring isn’t about adding concurrency.&lt;/p&gt;

&lt;p&gt;It’s about &lt;strong&gt;removing everything that doesn’t need to be there&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>php</category>
      <category>refactorit</category>
    </item>
    <item>
      <title>Small Pull Requests: Why They Move Development Teams Faster</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 17 Mar 2026 14:00:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/small-pull-requests-why-they-move-development-teams-faster-20kh</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/small-pull-requests-why-they-move-development-teams-faster-20kh</guid>
      <description>&lt;p&gt;In many backend teams, pull requests slowly grow into something nobody wants to review.&lt;/p&gt;

&lt;p&gt;A developer starts working on a feature.&lt;br&gt;
At first, it’s just a small change — maybe a new endpoint or a service update.&lt;/p&gt;

&lt;p&gt;Soon another improvement gets added.&lt;/p&gt;

&lt;p&gt;A small refactor follows.&lt;/p&gt;

&lt;p&gt;Finally, a fix for something unrelated appears.&lt;/p&gt;

&lt;p&gt;Two days later, the pull request contains &lt;strong&gt;900 lines of changes across 14 files.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At that moment, something predictable happens.&lt;/p&gt;

&lt;p&gt;The review slows down.&lt;/p&gt;

&lt;p&gt;Not because the reviewers are lazy — but because &lt;strong&gt;large pull requests create cognitive overload.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And once reviews slow down, the entire development workflow begins to lose momentum.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Psychological Problem of Large Pull Requests
&lt;/h2&gt;

&lt;p&gt;Large pull requests create a subtle psychological effect.&lt;/p&gt;

&lt;p&gt;When a reviewer opens a PR and sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+824 −217 changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;their brain immediately categorizes it as &lt;strong&gt;expensive work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The result is predictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The review gets postponed.&lt;/li&gt;
&lt;li&gt;The reviewer waits for “more time”.&lt;/li&gt;
&lt;li&gt;The PR sits for hours or days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, when someone finally reviews it, they cannot realistically analyze everything in depth.&lt;/p&gt;

&lt;p&gt;So one of two things happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The reviewer skims the code and approves it quickly.&lt;/li&gt;
&lt;li&gt;The reviewer leaves many comments that trigger long discussion threads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither outcome improves delivery speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Pull Requests Change the Dynamic
&lt;/h2&gt;

&lt;p&gt;Now compare that to a PR that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+42 −8 changes

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This feels manageable.&lt;/p&gt;

&lt;p&gt;A reviewer can realistically read the entire diff in a few minutes.&lt;/p&gt;

&lt;p&gt;This leads to several important effects:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Faster Reviews
&lt;/h2&gt;

&lt;p&gt;Small PRs get reviewed faster simply because they feel easier.&lt;/p&gt;

&lt;p&gt;Many teams see review times drop from &lt;strong&gt;days to hours&lt;/strong&gt; after adopting smaller pull requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Better Feedback
&lt;/h2&gt;

&lt;p&gt;When the code is smaller, reviewers can focus on &lt;strong&gt;design decisions instead of scanning files.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of writing comments like:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“This file changed a lot, can you explain the logic?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;they can ask more meaningful questions:&lt;/p&gt;

&lt;p&gt;“&lt;em&gt;Should this logic live in the service layer instead?”&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Less Risk
&lt;/h2&gt;

&lt;p&gt;Large pull requests often hide bugs.&lt;/p&gt;

&lt;p&gt;For example, reviewers may miss subtle mistakes when hundreds of lines change at once.&lt;/p&gt;

&lt;p&gt;Small PRs isolate changes.&lt;/p&gt;

&lt;p&gt;If a bug appears, it’s easier to trace it back to a specific change.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Example
&lt;/h2&gt;

&lt;p&gt;Consider a backend developer implementing a new feature:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send notification emails when an order is shipped.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A typical large PR might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database migration&lt;/li&gt;
&lt;li&gt;new notification service&lt;/li&gt;
&lt;li&gt;email template&lt;/li&gt;
&lt;li&gt;queue job&lt;/li&gt;
&lt;li&gt;controller changes&lt;/li&gt;
&lt;li&gt;unit tests&lt;/li&gt;
&lt;li&gt;refactoring an old helper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All combined into &lt;strong&gt;one big pull request.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead, this can be split into several smaller ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR 1 – Database Migration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add shipped_at column to orders table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple schema change.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR 2 – Notification Service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create OrderShipmentNotifier service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure backend logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR 3 – Queue Job
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add SendShipmentEmail job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Background processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR 4 – Controller Integration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trigger notification when order is shipped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integration step.&lt;/p&gt;

&lt;p&gt;Each PR becomes &lt;strong&gt;reviewable within minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The entire feature moves through the pipeline &lt;strong&gt;much faster&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Benefit: Continuous Integration
&lt;/h2&gt;

&lt;p&gt;Small pull requests also improve CI pipelines.&lt;/p&gt;

&lt;p&gt;Large PRs often cause several problems:&lt;/p&gt;

&lt;p&gt;When a small PR fails CI, the cause is usually obvious.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PR title: Add OrderShipmentNotifier service
CI failure: NotificationServiceTest fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is straightforward.&lt;/p&gt;

&lt;p&gt;Compare this to a massive PR with dozens of changes — the failure may come from anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Resist Small Pull Requests
&lt;/h2&gt;

&lt;p&gt;Despite the benefits, many developers hesitate to split their work.&lt;/p&gt;

&lt;p&gt;Common reasons include:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;“The feature isn’t finished yet.”&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;This is the most common argument.&lt;/p&gt;

&lt;p&gt;But many PRs &lt;strong&gt;do not need to represent a finished feature.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They only need to represent a safe incremental step.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;You can merge a service class before it’s used anywhere.&lt;/p&gt;

&lt;p&gt;That’s perfectly valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;“It creates too many PRs.”&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;This concern sounds logical but rarely holds in practice.&lt;/p&gt;

&lt;p&gt;Ten small PRs usually move through the system faster than one large one.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because multiple reviewers can process them quickly instead of blocking on one huge change.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;“It’s extra work.”&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;At first, splitting work into smaller PRs requires discipline.&lt;/p&gt;

&lt;p&gt;But after a few weeks, it becomes natural.&lt;/p&gt;

&lt;p&gt;Developers start thinking in &lt;strong&gt;incremental changes&lt;/strong&gt; rather than large feature drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Rule That Works
&lt;/h2&gt;

&lt;p&gt;Some teams use a simple heuristic:&lt;/p&gt;

&lt;p&gt;If a pull request takes more than &lt;strong&gt;10 minutes to review&lt;/strong&gt;, it is probably too large.&lt;/p&gt;

&lt;p&gt;This isn’t a strict rule, but it works surprisingly well.&lt;/p&gt;

&lt;p&gt;Another guideline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200–300 lines of diff&lt;/strong&gt; should usually be the upper limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond that, review quality drops quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Long-Term Impact on Workflow
&lt;/h2&gt;

&lt;p&gt;When teams consistently use small pull requests, several improvements appear over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline becomes predictable
&lt;/h2&gt;

&lt;p&gt;PRs move steadily through the system instead of forming large queues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developers ship code more often
&lt;/h2&gt;

&lt;p&gt;Frequent merges reduce integration conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code reviews become collaborative
&lt;/h2&gt;

&lt;p&gt;Instead of being a bottleneck, reviews become quick feedback loops.&lt;/p&gt;

&lt;p&gt;And perhaps most importantly:&lt;/p&gt;

&lt;p&gt;The team starts focusing on &lt;strong&gt;continuous delivery instead of large feature dumps.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Many teams try to improve their development workflow by introducing new tools, processes, or complex CI pipelines.&lt;/p&gt;

&lt;p&gt;But one of the most effective improvements is surprisingly simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep pull requests small.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Small pull requests reduce friction in every stage of development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code review&lt;/li&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;li&gt;debugging&lt;/li&gt;
&lt;li&gt;merging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They allow teams to maintain momentum — and momentum is often the most valuable resource in software development.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>software</category>
      <category>ci</category>
      <category>programming</category>
    </item>
    <item>
      <title>Laravel Queue Testing: What Most Developers Get Wrong</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 10 Mar 2026 14:00:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/laravel-queue-testing-what-most-developers-get-wrong-45oj</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/laravel-queue-testing-what-most-developers-get-wrong-45oj</guid>
      <description>&lt;p&gt;Queues are where “it works on my machine” quietly turns into production incidents.&lt;/p&gt;

&lt;p&gt;Emails are sent in the background.&lt;br&gt;
Invoices are generated asynchronously.&lt;br&gt;
External APIs are called outside the request lifecycle.&lt;br&gt;
Data synchronization runs in workers you don’t actively watch.&lt;/p&gt;

&lt;p&gt;Laravel makes queues extremely easy to use. Add ShouldQueue, dispatch the job, done.&lt;/p&gt;

&lt;p&gt;Testing them properly is a different story.&lt;/p&gt;

&lt;p&gt;Most developers stop at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bus::fake();
Bus::assertDispatched(SyncOrderJob::class);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That verifies wiring. It does &lt;strong&gt;not&lt;/strong&gt; verify behavior.&lt;/p&gt;

&lt;p&gt;This article focuses on how to test Laravel jobs correctly — including idempotency, failure handling, and retry safety — with a realistic example.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scenario: Syncing an Order to an External API
&lt;/h2&gt;

&lt;p&gt;Let’s say we have an Order model that needs to be synchronized to an external system after checkout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Job&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace App\Jobs;use App\Models\Order;
use App\Services\OrderApiClient;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;class SyncOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, SerializesModels;    public int $tries = 3;    public function __construct(
        public Order $order
    ) {}    public function handle(OrderApiClient $client): void
    {
        if ($this-&amp;gt;order-&amp;gt;synced_at !== null) {
            return; // idempotency guard
        }        $client-&amp;gt;send([
            'id' =&amp;gt; $this-&amp;gt;order-&amp;gt;id,
            'total' =&amp;gt; $this-&amp;gt;order-&amp;gt;total,
        ]);        $this-&amp;gt;order-&amp;gt;update([
            'synced_at' =&amp;gt; now(),
        ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Already we have several important concerns:&lt;/p&gt;

&lt;p&gt;The job must not sync twice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It depends on an external API client.&lt;/li&gt;
&lt;li&gt;It may be retried.&lt;/li&gt;
&lt;li&gt;It mutates persistent state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s test it properly&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Testing Dispatch (Integration Level)
&lt;/h2&gt;

&lt;p&gt;This belongs in a feature test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use Illuminate\Support\Facades\Bus;
use App\Jobs\SyncOrderJob;

public function test_order_dispatches_sync_job()
{
    Bus::fake();

    $order = Order::factory()-&amp;gt;create();

    $order-&amp;gt;markAsPaid(); // imagine this dispatches the job

    Bus::assertDispatched(SyncOrderJob::class, function ($job) use ($order) {
        return $job-&amp;gt;order-&amp;gt;is($order);
    });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good. Necessary.&lt;/p&gt;

&lt;p&gt;But insufficient.&lt;/p&gt;

&lt;p&gt;This does not test &lt;em&gt;handle()&lt;/em&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Testing Job Logic Directly (Unit Level)
&lt;/h2&gt;

&lt;p&gt;Now we test the job itself.&lt;/p&gt;

&lt;p&gt;We do not fake the bus.&lt;br&gt;
We instantiate the job and call handle() directly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Mocking the External API
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use App\Services\OrderApiClient;
use Mockery;

public function test_it_calls_external_api_when_not_synced()
{
    $order = Order::factory()-&amp;gt;create([
        'synced_at' =&amp;gt; null,
    ]);    

    $mock = Mockery::mock(OrderApiClient::class);

    $mock-&amp;gt;shouldReceive('send')
        -&amp;gt;once()
        -&amp;gt;with(Mockery::on(fn ($payload) =&amp;gt; $payload['id'] === $order-&amp;gt;id));    

    $job = new SyncOrderJob($order);    

    $job-&amp;gt;handle($mock);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This verifies real behavior.&lt;/p&gt;

&lt;p&gt;We are not testing Laravel.&lt;br&gt;
We are testing our business logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Testing Idempotency
&lt;/h2&gt;

&lt;p&gt;Queues retry automatically. If your job is not idempotent, you will create duplicate side effects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function test_it_does_not_call_api_if_already_synced()
{
    $order = Order::factory()-&amp;gt;create([
        'synced_at' =&amp;gt; now(),
    ]);    

    $mock = Mockery::mock(OrderApiClient::class);

    $mock-&amp;gt;shouldNotReceive('send');    

    $job = new SyncOrderJob($order);    

    $job-&amp;gt;handle($mock);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this test fails, your production system will eventually duplicate work.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Testing Retry Safety
&lt;/h2&gt;

&lt;p&gt;Consider a failure between the API call and the database update.&lt;/p&gt;

&lt;p&gt;If the job crashes after calling the API but before marking the order as synced, retry will send it again.&lt;/p&gt;

&lt;p&gt;Safer pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function handle(OrderApiClient $client): void
{
    if ($this-&amp;gt;order-&amp;gt;synced_at !== null) {
        return;
    }    $this-&amp;gt;order-&amp;gt;update([
        'synced_at' =&amp;gt; now(),
    ]);    $client-&amp;gt;send([
        'id' =&amp;gt; $this-&amp;gt;order-&amp;gt;id,
        'total' =&amp;gt; $this-&amp;gt;order-&amp;gt;total,
    ]);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now state changes before side effects.&lt;/p&gt;

&lt;p&gt;Let’s simulate a failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function test_it_retries_safely()
{
    $order = Order::factory()-&amp;gt;create([
        'synced_at' =&amp;gt; null,
    ]);

    $mock = Mockery::mock(OrderApiClient::class);
    $mock-&amp;gt;shouldReceive('send')
        -&amp;gt;once()
        -&amp;gt;andThrow(new RuntimeException());

    $job = new SyncOrderJob($order);

    try {
        $job-&amp;gt;handle($mock);
    } catch (RuntimeException $e) {
        // expected
    }

    $order-&amp;gt;refresh();

    $this-&amp;gt;assertNotNull($order-&amp;gt;synced_at);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now even if retry happens, the idempotency guard prevents double execution.&lt;/p&gt;

&lt;p&gt;This is not theoretical. This is how duplicate invoices happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Testing Jobs That Dispatch Other Jobs
&lt;/h2&gt;

&lt;p&gt;Chained or nested jobs add complexity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ProcessPaymentJob::dispatch($order);
SendInvoiceJob::dispatch($order);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test that correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bus::fake();

$job = new ProcessPaymentJob($order);

$job-&amp;gt;handle($paymentService);

Bus::assertDispatched(SendInvoiceJob::class);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We test &lt;em&gt;handle()&lt;/em&gt; directly.&lt;/li&gt;
&lt;li&gt;We fake the bus only to assert nested dispatch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation is critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Testing Backoff and Retry Configuration
&lt;/h2&gt;

&lt;p&gt;Laravel allows configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public int $tries = 5;

public function backoff(): array
{
    return [10, 30, 60];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can test configuration explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function test_job_has_correct_retry_settings()
{
    $job = new SyncOrderJob(Order::factory()-&amp;gt;make());

    $this-&amp;gt;assertEquals(3, $job-&amp;gt;tries);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not glamorous — but configuration errors cause real outages.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The Dangerous Anti-Pattern
&lt;/h2&gt;

&lt;p&gt;This is common:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queue::fake();

SyncOrderJob::dispatch($order);

Queue::assertPushed(SyncOrderJob::class);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test will pass even if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The job throws immediately.&lt;/li&gt;
&lt;li&gt;The API client is broken.&lt;/li&gt;
&lt;li&gt;The logic is inverted.&lt;/li&gt;
&lt;li&gt;Idempotency is missing.&lt;/li&gt;
&lt;li&gt;The job deletes the order by accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You tested dispatch, not behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use &lt;em&gt;Bus::fake()&lt;/em&gt; vs Real Execution
&lt;/h2&gt;

&lt;p&gt;Use &lt;em&gt;Bus::fake()&lt;/em&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing controllers or services that dispatch jobs.&lt;/li&gt;
&lt;li&gt;Verifying orchestration.&lt;/li&gt;
&lt;li&gt;Ensuring a job is queued under certain conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not use &lt;em&gt;Bus::fake()&lt;/em&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing job business logic.&lt;/li&gt;
&lt;li&gt;Testing external integration behavior.&lt;/li&gt;
&lt;li&gt;Testing failure handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In job tests, instantiate and call &lt;em&gt;handle()&lt;/em&gt; directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: Running Jobs Synchronously in Tests
&lt;/h2&gt;

&lt;p&gt;Laravel allows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config(['queue.default' =&amp;gt; 'sync']);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This executes jobs immediately.&lt;/p&gt;

&lt;p&gt;Useful for feature tests where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want full behavior executed.&lt;/li&gt;
&lt;li&gt;You don’t care about queue infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But be careful:&lt;/p&gt;

&lt;p&gt;If you rely on sync execution everywhere, you may miss race conditions or retry issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Principles for Testable Jobs
&lt;/h2&gt;

&lt;p&gt;If your job is hard to test, it’s usually poorly designed.&lt;/p&gt;

&lt;p&gt;Good Laravel jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inject services via handle().&lt;/li&gt;
&lt;li&gt;Keep constructors lightweight.&lt;/li&gt;
&lt;li&gt;Avoid heavy logic in __construct.&lt;/li&gt;
&lt;li&gt;Avoid static service calls.&lt;/li&gt;
&lt;li&gt;Are idempotent by design.&lt;/li&gt;
&lt;li&gt;Fail loudly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad Laravel jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Call Http::post() directly inside handle.&lt;/li&gt;
&lt;li&gt;Query models statically without abstraction.&lt;/li&gt;
&lt;li&gt;Mutate global state.&lt;/li&gt;
&lt;li&gt;Swallow exceptions silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing reveals architecture quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Queues introduce three new axes of complexity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Time (execution is delayed)&lt;/li&gt;
&lt;li&gt;Failure (automatic retries)&lt;/li&gt;
&lt;li&gt;Concurrency (multiple workers)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Testing Laravel jobs properly means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing dispatch at feature level.&lt;/li&gt;
&lt;li&gt;Testing handle() directly at unit level.&lt;/li&gt;
&lt;li&gt;Mocking external services.&lt;/li&gt;
&lt;li&gt;Verifying idempotency.&lt;/li&gt;
&lt;li&gt;Simulating failure.&lt;/li&gt;
&lt;li&gt;Thinking explicitly about retry safety.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Queues scale your application.&lt;/p&gt;

&lt;p&gt;Tests make your background processing reliable.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>laravel</category>
      <category>php</category>
      <category>testing</category>
    </item>
    <item>
      <title>Singleton Pattern in PHP: Refactoring Global State the Right Way</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 03 Mar 2026 14:00:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/singleton-pattern-in-php-refactoring-global-state-the-right-way-1gbl</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/singleton-pattern-in-php-refactoring-global-state-the-right-way-1gbl</guid>
      <description>&lt;p&gt;In many legacy PHP codebases, global state sneaks in quietly.&lt;br&gt;
A config.php file is included everywhere.&lt;br&gt;
A static helper class grows until it becomes a god object.&lt;br&gt;
A database connection lives in a global variable “just for now”.&lt;/p&gt;

&lt;p&gt;At first, it feels convenient.&lt;br&gt;
Later, it becomes untestable, unpredictable, and painful to change.&lt;/p&gt;

&lt;p&gt;Let’s look at how global state often appears in PHP projects, how developers usually try to “fix” it, and how refactoring toward a Singleton can help — but also why Singleton is frequently misused.&lt;/p&gt;

&lt;p&gt;Previously article in this category: &lt;a href="https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Code Smell: Hidden Global State
&lt;/h2&gt;

&lt;p&gt;A typical example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// config.php
return [
    'db_host' =&amp;gt; 'localhost',
    'db_user' =&amp;gt; 'root',
    'db_pass' =&amp;gt; 'secret',
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// bootstrap.php
$config = require 'config.php';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// UserRepository.php
class UserRepository
{
    public function find(int $id): array
    {
        global $config;        $conn = new PDO(
            'mysql:host=' . $config['db_host'],
            $config['db_user'],
            $config['db_pass']
        );        return $conn-&amp;gt;query('SELECT * FROM users WHERE id = ' . $id)-&amp;gt;fetch();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code “works”.&lt;br&gt;
It also hides a dependency.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;UserRepository&lt;/em&gt; depends on configuration and a database connection, but nothing in its API communicates that. The dependency is invisible and implicit.&lt;/p&gt;

&lt;p&gt;This leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unpredictable behavior in tests&lt;/li&gt;
&lt;li&gt;coupling between unrelated parts of the system&lt;/li&gt;
&lt;li&gt;accidental mutation of shared state&lt;/li&gt;
&lt;li&gt;code that is hard to reason about&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a classic code smell: &lt;strong&gt;hidden dependencies via global state.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Naive Fix: Static Helper Class
&lt;/h2&gt;

&lt;p&gt;The next evolutionary step in many PHP projects looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Config
{
    private static array $data;    public static function load(): void
    {
        self::$data = require 'config.php';
    }    public static function get(string $key): mixed
    {
        return self::$data[$key];
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class UserRepository
{
    public function find(int $id): array
    {
        $conn = new PDO(
            'mysql:host=' . Config::get('db_host'),
            Config::get('db_user'),
            Config::get('db_pass')
        );        return $conn-&amp;gt;query('SELECT * FROM users WHERE id = ' . $id)-&amp;gt;fetch();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks cleaner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no global&lt;/li&gt;
&lt;li&gt;no messy includes&lt;/li&gt;
&lt;li&gt;centralized config access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But architecturally, nothing really improved.&lt;/p&gt;

&lt;p&gt;This is still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;global state&lt;/li&gt;
&lt;li&gt;hidden dependency&lt;/li&gt;
&lt;li&gt;impossible to replace in tests&lt;/li&gt;
&lt;li&gt;tightly coupled to a static API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We replaced global with a static singleton-like façade.&lt;/p&gt;

&lt;p&gt;This is not refactoring.&lt;br&gt;
This is just changing syntax.&lt;/p&gt;
&lt;h2&gt;
  
  
  Refactoring Toward a Real Singleton
&lt;/h2&gt;

&lt;p&gt;Let’s refactor this step by step into a proper Singleton.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class Config
{
    private static ?Config $instance = null;
    private array $data;    private function __construct()
    {
        $this-&amp;gt;data = require 'config.php';
    }    public static function getInstance(): Config
    {
        if (self::$instance === null) {
            self::$instance = new Config();
        }        return self::$instance;
    }    public function get(string $key): mixed
    {
        return $this-&amp;gt;data[$key] ?? null;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class UserRepository
{
    public function find(int $id): array
    {
        $config = Config::getInstance();        $conn = new PDO(
            'mysql:host=' . $config-&amp;gt;get('db_host'),
            $config-&amp;gt;get('db_user'),
            $config-&amp;gt;get('db_pass')
        );        return $conn-&amp;gt;query('SELECT * FROM users WHERE id = ' . $id)-&amp;gt;fetch();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is now a “proper” Singleton:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;private constructor&lt;/li&gt;
&lt;li&gt;lazy initialization&lt;/li&gt;
&lt;li&gt;controlled access point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We improved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;structure&lt;/li&gt;
&lt;li&gt;encapsulation&lt;/li&gt;
&lt;li&gt;testability (slightly)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But we did not fix the core design issue.&lt;/p&gt;

&lt;p&gt;The dependency is still hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem: Singleton Is Global State in Disguise
&lt;/h2&gt;

&lt;p&gt;A Singleton is not dependency injection.&lt;br&gt;
It is &lt;strong&gt;global state with better manners&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You still cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pass different configurations in tests&lt;/li&gt;
&lt;li&gt;swap implementations easily&lt;/li&gt;
&lt;li&gt;reason locally about dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From an architectural perspective, &lt;em&gt;UserRepository&lt;/em&gt; still depends on &lt;em&gt;Config&lt;/em&gt;, but that dependency is invisible in its constructor or method signature.&lt;/p&gt;

&lt;p&gt;This creates tight coupling and invisible control flow.&lt;/p&gt;

&lt;p&gt;The code is cleaner — but not better designed.&lt;/p&gt;
&lt;h2&gt;
  
  
  When Singleton Is Actually Legitimate
&lt;/h2&gt;

&lt;p&gt;Singleton is not evil by definition.&lt;br&gt;
It is evil when used as a default container for everything.&lt;/p&gt;

&lt;p&gt;Legitimate use cases in PHP:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Immutable Application Configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class AppConfig
{
    private static ?AppConfig $instance = null;
    private array $values;    private function __construct(array $values)
    {
        $this-&amp;gt;values = $values;
    }    public static function boot(array $values): void
    {
        self::$instance = new self($values);
    }    public static function getInstance(): AppConfig
    {
        return self::$instance;
    }    public function get(string $key): mixed
    {
        return $this-&amp;gt;values[$key] ?? null;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bootstrapped once, read-only afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Logger&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A logger is often a cross-cutting concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shared&lt;/li&gt;
&lt;li&gt;stateless&lt;/li&gt;
&lt;li&gt;global by nature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Feature Flags / Environment Context&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the state is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;immutable&lt;/li&gt;
&lt;li&gt;read-only&lt;/li&gt;
&lt;li&gt;environment-level&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Singleton can be acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Singleton Is a Design Smell
&lt;/h2&gt;

&lt;p&gt;Singleton becomes a problem when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it contains mutable domain state&lt;/li&gt;
&lt;li&gt;it replaces proper dependency injection&lt;/li&gt;
&lt;li&gt;it hides real architectural boundaries&lt;/li&gt;
&lt;li&gt;it becomes a service locator&lt;/li&gt;
&lt;li&gt;business logic starts living inside it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your Singleton has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20 methods&lt;/li&gt;
&lt;li&gt;multiple responsibilities&lt;/li&gt;
&lt;li&gt;domain logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you created a god object with a private constructor.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Refactoring Path
&lt;/h2&gt;

&lt;p&gt;Often, Singleton should be treated as a &lt;strong&gt;transitional refactoring step&lt;/strong&gt;, not a final architecture.&lt;/p&gt;

&lt;p&gt;Better end state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class UserRepository
{
    public function __construct(
        private PDO $connection
    ) {}    public function find(int $id): array
    {
        return $this-&amp;gt;connection
            -&amp;gt;query('SELECT * FROM users WHERE id = ' . $id)
            -&amp;gt;fetch();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And composition happens at the boundary of the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$config = Config::getInstance();$pdo = new PDO(
    'mysql:host=' . $config-&amp;gt;get('db_host'),
    $config-&amp;gt;get('db_user'),
    $config-&amp;gt;get('db_pass')
);$repo = new UserRepository($pdo);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dependencies are explicit&lt;/li&gt;
&lt;li&gt;testing is trivial&lt;/li&gt;
&lt;li&gt;coupling is controlled&lt;/li&gt;
&lt;li&gt;architecture is visible&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Singleton is not a goal.&lt;br&gt;
It is a compromise.&lt;/p&gt;

&lt;p&gt;It can be a useful refactoring step when removing global state from a legacy PHP codebase.&lt;br&gt;
It can make dependencies slightly more explicit.&lt;br&gt;
It can improve encapsulation.&lt;/p&gt;

&lt;p&gt;But if you stop there, you only replaced chaos with a nicer-looking global variable.&lt;/p&gt;

&lt;p&gt;Good design does not start with patterns.&lt;br&gt;
It starts with &lt;strong&gt;making dependencies explicit&lt;/strong&gt; and &lt;strong&gt;pushing composition to the edges of your system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Singleton is a tool.&lt;br&gt;
Use it deliberately — and sparingly.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>softwareengineering</category>
      <category>php</category>
      <category>refactoring</category>
    </item>
    <item>
      <title>Why “Just One More Quick Fix” Destroys Your Delivery Pipeline</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 24 Feb 2026 14:00:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/why-just-one-more-quick-fix-destroys-your-delivery-pipeline-4927</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/why-just-one-more-quick-fix-destroys-your-delivery-pipeline-4927</guid>
      <description>&lt;p&gt;Every development team has heard this sentence:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Let’s just push one more quick fix.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It sounds harmless. Responsible, even. Something small broke, a customer is affected, production is slightly off — and the fix is obvious. Five minutes of work. No need to go through the full process. No need to wait for code review. No need to bother QA. Just one tiny change to keep things moving.&lt;/p&gt;

&lt;p&gt;The problem is not the quick fix itself.&lt;br&gt;
The problem is the culture that grows around it.&lt;/p&gt;

&lt;p&gt;Over time, “just one more quick fix” becomes the default way changes enter production. And once that happens, your delivery pipeline slowly collapses — not because anyone is incompetent, but because the system you built to protect quality gets bypassed piece by piece.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/01/31/why-almost-done-work-breaks-development-flow/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/01/31/why-almost-done-work-breaks-development-flow/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Quick Fixes Become the Normal Path to Production
&lt;/h2&gt;

&lt;p&gt;Most teams don’t start with bad intentions. The first few quick fixes usually happen under real pressure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A critical bug blocks a customer.&lt;/li&gt;
&lt;li&gt;A payment fails.&lt;/li&gt;
&lt;li&gt;An integration with a third-party API breaks overnight.&lt;/li&gt;
&lt;li&gt;A configuration mistake takes down part of production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this stage, bypassing the process feels justified. You skip review because “it’s just a one-line change.” You deploy manually because the CI pipeline takes too long. You test in production because the bug only reproduces there.&lt;/p&gt;

&lt;p&gt;Nothing explodes. The fix works. The customer is happy. The team moves on.&lt;/p&gt;

&lt;p&gt;That success teaches the wrong lesson.&lt;/p&gt;

&lt;p&gt;The next time something small breaks, the team remembers:&lt;br&gt;
“We fixed it quickly last time. Let’s do it the same way.”&lt;/p&gt;

&lt;p&gt;After a few months, you end up with two delivery paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The official pipeline: PR → review → tests → CI → deploy&lt;/li&gt;
&lt;li&gt;The real pipeline: Slack message → quick commit → direct deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second one becomes the faster, socially accepted option. The first one starts to feel like bureaucracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Damage: You Don’t See the Cost Immediately
&lt;/h2&gt;

&lt;p&gt;The real damage of quick fixes is not in the individual changes. It’s in what they do to your system over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Your CI Pipeline Stops Being Trustworthy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once people regularly bypass CI, the pipeline stops reflecting reality. Builds fail for reasons nobody prioritizes fixing. Tests become flaky and get ignored. Warnings pile up.&lt;/p&gt;

&lt;p&gt;Eventually, developers stop trusting the pipeline at all.&lt;/p&gt;

&lt;p&gt;At that point, CI is no longer a quality gate — it’s just a ritual.&lt;br&gt;
Teams running on platforms like GitHub or GitLab often reach this state not because the tools are bad, but because the workflow culture quietly degrades around them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Code Reviews Become Optional (Which Means Useless)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When urgent fixes bypass review, reviews lose their authority. Review becomes something you do for “normal work,” while “important work” skips it.&lt;/p&gt;

&lt;p&gt;That’s a subtle but dangerous shift.&lt;/p&gt;

&lt;p&gt;You’re effectively teaching the team that quality checks are less important when stakes are higher. Over time, this creates blind spots exactly where you need discipline the most: production-critical paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You Normalize Risk Without Noticing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every quick fix that works without immediate consequences normalizes risk-taking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploying without review becomes acceptable.&lt;/li&gt;
&lt;li&gt;Deploying without tests becomes acceptable.&lt;/li&gt;
&lt;li&gt;Deploying without understanding side effects becomes 
acceptable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The absence of incidents is interpreted as proof that the process is unnecessary — until the day it fails in a way that costs real money, data, or trust.&lt;/p&gt;

&lt;p&gt;By the time that incident happens, the delivery pipeline is already too eroded to protect you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Psychological Trap: Quick Fixes Feel Like Ownership
&lt;/h2&gt;

&lt;p&gt;Quick fixes often feel good on a personal level.&lt;/p&gt;

&lt;p&gt;You fixed a production issue fast.&lt;br&gt;
You unblocked a customer.&lt;br&gt;
You were the hero who saved the day.&lt;/p&gt;

&lt;p&gt;This creates a subtle incentive problem: the system rewards individuals for bypassing the process. The delivery pipeline, which exists to protect the team and the product, becomes an obstacle to individual heroics.&lt;/p&gt;

&lt;p&gt;Over time, teams drift into a culture where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Being fast matters more than being safe.&lt;/li&gt;
&lt;li&gt;Fixing symptoms is rewarded more than fixing root causes.&lt;/li&gt;
&lt;li&gt;Short-term relief beats long-term stability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how delivery pipelines rot — not from neglect, but from well-meaning people optimizing for speed under pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Pattern: The “Temporary” Hotfix That Becomes Permanent
&lt;/h2&gt;

&lt;p&gt;A common pattern looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A bug appears in production.&lt;/li&gt;
&lt;li&gt;Someone applies a quick fix directly in production or through a rushed commit.&lt;/li&gt;
&lt;li&gt;The fix is labeled “temporary.”&lt;/li&gt;
&lt;li&gt;The team plans to clean it up later.&lt;/li&gt;
&lt;li&gt;Later never comes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weeks later, another change interacts with that temporary fix and breaks something else. Now nobody fully understands the code path. The pipeline didn’t catch it because the fix never went through proper tests.&lt;/p&gt;

&lt;p&gt;The original quick fix saved 30 minutes.&lt;br&gt;
The long-term cost is measured in hours of debugging, fragile code, and lost confidence in the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Destroys Flow (Not Just Quality)
&lt;/h2&gt;

&lt;p&gt;Quick fixes don’t just hurt quality. They destroy delivery flow.&lt;/p&gt;

&lt;p&gt;Once bypassing the pipeline becomes normal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers hesitate to touch fragile areas.&lt;/li&gt;
&lt;li&gt;Releases become unpredictable.&lt;/li&gt;
&lt;li&gt;Small changes cause big side effects.&lt;/li&gt;
&lt;li&gt;People slow down because they don’t trust the system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ironically, the behavior that started as “moving faster” ends up making delivery slower and more stressful.&lt;/p&gt;

&lt;p&gt;You trade short-term speed for long-term friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Fix the Culture Without Becoming Bureaucratic
&lt;/h2&gt;

&lt;p&gt;This isn’t about banning hotfixes. Production incidents are real. Speed matters. The goal is to make &lt;em&gt;fast&lt;/em&gt; changes also &lt;em&gt;safe&lt;/em&gt; changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Make the Fast Path the Safe Path&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your CI takes 40 minutes, people will bypass it.&lt;br&gt;
If your review process blocks urgent fixes, people will bypass it.&lt;/p&gt;

&lt;p&gt;Your pipeline must be optimized for speed in emergencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lightweight reviews for hotfixes.&lt;/li&gt;
&lt;li&gt;Fast CI path for critical changes.&lt;/li&gt;
&lt;li&gt;Clear “hotfix” workflow that still enforces minimal quality gates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Track Bypasses as Technical Debt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every bypass should leave a visible trace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A ticket to add tests later.&lt;/li&gt;
&lt;li&gt;A follow-up task to clean up the quick fix.&lt;/li&gt;
&lt;li&gt;A post-incident note explaining what was skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If bypassing the pipeline leaves no trace, it becomes invisible debt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Reward Stability, Not Heroics&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Publicly recognize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixes that go through the proper pipeline under pressure.&lt;/li&gt;
&lt;li&gt;Improvements to CI speed.&lt;/li&gt;
&lt;li&gt;Reductions in flaky tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop glorifying the person who “just pushed a fix to prod” without safeguards. You want to reward people who improve the system, not just fight its symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Insight
&lt;/h2&gt;

&lt;p&gt;“Just one more quick fix” is never just one.&lt;/p&gt;

&lt;p&gt;It’s a small exception that rewires how your team relates to the delivery pipeline.&lt;br&gt;
It teaches people what the real process is — not the one in documentation, but the one that gets things shipped.&lt;/p&gt;

&lt;p&gt;If your real process bypasses quality gates under pressure, then under real pressure, you don’t have a delivery pipeline at all.&lt;/p&gt;

&lt;p&gt;You have hope and heroics.&lt;/p&gt;

&lt;p&gt;And that’s not a strategy.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>development</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Contract Testing External APIs in PHP with Pact (Real Laravel Example)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Wed, 18 Feb 2026 14:05:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/contract-testing-external-apis-in-php-with-pact-real-laravel-example-o3g</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/contract-testing-external-apis-in-php-with-pact-real-laravel-example-o3g</guid>
      <description>&lt;p&gt;Testing integrations with external APIs is one of the most fragile parts of any web application. In theory, we write tests, mock HTTP clients, and feel confident. In practice, APIs change, fields disappear, status codes shift, and production breaks anyway.&lt;/p&gt;

&lt;p&gt;I learned this the hard way.&lt;/p&gt;

&lt;p&gt;In one project, all my tests were green. My mocks returned exactly what I expected. A week later, the external API removed one field from the response. My mocks didn’t know about the change. Production did.&lt;/p&gt;

&lt;p&gt;Mocks told me everything was fine. Reality disagreed.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of problem &lt;strong&gt;contract testing&lt;/strong&gt; is meant to solve. In this article, I’ll show you how to use contract testing for external APIs in PHP with &lt;strong&gt;Pact&lt;/strong&gt;, using a real-world *&lt;em&gt;Laravel *&lt;/em&gt; example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Mocking External APIs Is Not Enough
&lt;/h2&gt;

&lt;p&gt;Mocking external APIs is useful. I still do it. It makes tests fast, deterministic, and cheap. But mocks have one fatal flaw:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;They only test your assumptions about the API, not the API itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Typical problems I’ve seen in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API removes or renames a field.&lt;/li&gt;
&lt;li&gt;A nested structure changes shape.&lt;/li&gt;
&lt;li&gt;The API starts returning 404 instead of 200 in some edge cases.&lt;/li&gt;
&lt;li&gt;The API adds a required field to the request body.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your mocks will never catch this unless you manually update them. That means mocks alone can silently drift away from reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Contract Testing (In Plain English)
&lt;/h2&gt;

&lt;p&gt;Contract testing sits between mocking and full integration tests.&lt;/p&gt;

&lt;p&gt;Instead of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This is what I think the API returns,”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;you say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This is the contract between my app (consumer) and the API (provider).”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With consumer-driven contract testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;consumer&lt;/strong&gt;&lt;br&gt;
defines what it expects from the API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;provider&lt;/strong&gt; must verify that it actually fulfills this contract.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the provider changes something that breaks the contract, tests fail &lt;strong&gt;before production breaks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This turns breaking API changes from a runtime surprise into a build-time failure.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: How contract testing works between a Laravel consumer, Pact mock server, and the real API provider.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc61ic5cgcr1sokhuhen8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc61ic5cgcr1sokhuhen8.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Scenario: Laravel as API Consumer
&lt;/h2&gt;

&lt;p&gt;Let’s assume a Laravel application integrates with an external billing API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/customers/{id}

Response:
{
  "id": 123,
  "email": "john@example.com",
  "is_active": true
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Laravel service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class BillingClient
{
    public function getCustomer(int $id): array
    {
        $response = Http::get("https://billing.example.com/api/customers/{$id}");

        return $response-&amp;gt;json();
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your application relies on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email&lt;/li&gt;
&lt;li&gt;is_active&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the provider removes &lt;em&gt;is_active&lt;/em&gt;, your app breaks. Mocks won’t catch it. Contract testing will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step: Contract Testing with Pact in Laravel
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Installing Pact for PHP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install Pact dependencies for your test environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composer require --dev pact-foundation/pact-php

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll also need the Pact CLI running locally or in CI (usually via Docker).&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the Consumer Contract Test
&lt;/h2&gt;

&lt;p&gt;The goal: define what your Laravel app &lt;strong&gt;expects&lt;/strong&gt; from the API.&lt;/p&gt;

&lt;p&gt;Example consumer test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function test_it_fetches_customer_from_billing_api()
{
    $builder = new InteractionBuilder();

    $builder
        -&amp;gt;given('Customer 123 exists')
        -&amp;gt;uponReceiving('A request for customer 123')
        -&amp;gt;with([
            'method' =&amp;gt; 'GET',
            'path'   =&amp;gt; '/api/customers/123',
        ])
        -&amp;gt;willRespondWith([
            'status' =&amp;gt; 200,
            'headers' =&amp;gt; ['Content-Type' =&amp;gt; 'application/json'],
            'body' =&amp;gt; [
                'id' =&amp;gt; 123,
                'email' =&amp;gt; 'john@example.com',
                'is_active' =&amp;gt; true,
            ],
        ]);

    $builder-&amp;gt;verify(function () {
        $client = new BillingClient();
        $customer = $client-&amp;gt;getCustomer(123);

        $this-&amp;gt;assertSame('john@example.com', $customer['email']);
        $this-&amp;gt;assertTrue($customer['is_active']);
    });
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;defines the expected request&lt;/li&gt;
&lt;li&gt;defines the expected response&lt;/li&gt;
&lt;li&gt;generates a &lt;strong&gt;contract file&lt;/strong&gt; (Pact file)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This contract is the &lt;strong&gt;single source of truth&lt;/strong&gt; between consumer and provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the Contract on the Provider Side
&lt;/h2&gt;

&lt;p&gt;On the provider side, the API team runs Pact verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pact-verifier --provider-base-url=http://localhost:8000 \
 --pact-url=./pacts/billing-consumer.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the API no longer returns is_active, verification fails.&lt;/p&gt;

&lt;p&gt;This is the key difference from mocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mocks only test the consumer&lt;/li&gt;
&lt;li&gt;contract testing tests the agreement&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running Contract Tests in CI
&lt;/h2&gt;

&lt;p&gt;This is where contract testing becomes powerful.&lt;/p&gt;

&lt;p&gt;Typical CI flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consumer tests generate Pact contracts.&lt;/li&gt;
&lt;li&gt;Provider pipeline verifies contracts.&lt;/li&gt;
&lt;li&gt;Pipeline fails if contracts are broken.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This creates a safety net:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;API changes cannot be deployed if they break existing consumers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common Mistakes with Contract Testing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Over-Specifying Responses&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don’t lock down every field if you don’t need it.&lt;br&gt;
Only define what your app actually uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only Testing Happy Paths&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Include error contracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;404 responses&lt;/li&gt;
&lt;li&gt;validation errors&lt;/li&gt;
&lt;li&gt;unauthorized responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not Versioning Contracts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Treat contracts like code. Version them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not Enforcing Contracts in CI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If no pipeline enforces them, contract testing is just documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Contract Testing Is Worth It (and When It’s Overkill)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Contract testing is worth it when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple teams own different services&lt;/li&gt;
&lt;li&gt;APIs evolve frequently&lt;/li&gt;
&lt;li&gt;breaking changes are expensive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;It’s probably overkill when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the integration is trivial&lt;/li&gt;
&lt;li&gt;you don’t control the provider&lt;/li&gt;
&lt;li&gt;the API is stable and rarely changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contract testing is a scalpel, not a hammer. Use it where API stability matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Contract Testing Fits into Your Laravel Testing Strategy
&lt;/h2&gt;

&lt;p&gt;This is how I think about testing layers in real projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit tests – business logic&lt;/li&gt;
&lt;li&gt;Feature tests – HTTP flows&lt;/li&gt;
&lt;li&gt;Mocks – isolate slow or unstable services&lt;/li&gt;
&lt;li&gt;Contract tests – protect integrations from breaking changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mocks keep your tests fast.&lt;br&gt;
Contract tests keep your integrations honest.&lt;/p&gt;

&lt;p&gt;They solve different problems and work best together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Mocking external APIs is necessary — but it’s not sufficient.&lt;/p&gt;

&lt;p&gt;If your application depends on external services and you’ve ever been surprised by a breaking API change in production, contract testing is the missing safety net.&lt;/p&gt;

&lt;p&gt;You don’t need to contract-test everything. Start with one critical integration. Add one contract. Let your CI enforce it. The first time a breaking change is caught before deployment, contract testing pays for itself.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>Builder Pattern in PHP/Laravel: Building Clean and Flexible Order Objects</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 10 Feb 2026 14:05:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/builder-pattern-in-phplaravel-building-clean-and-flexible-order-objects-1mk7</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/builder-pattern-in-phplaravel-building-clean-and-flexible-order-objects-1mk7</guid>
      <description>&lt;p&gt;&lt;strong&gt;In practice&lt;/strong&gt;, orders in e-commerce systems often evolve over time, which makes them a perfect candidate for the Builder Pattern.&lt;/p&gt;

&lt;p&gt;Creating orders in an e-commerce application often seems straightforward at first: a customer selects items, adds them to a cart, and checks out. But as your application grows, the &lt;strong&gt;Order class&lt;/strong&gt; can quickly become complex. You might need to handle optional discounts, shipping options, gift wrapping, different payment methods, and more. Before long, your constructor looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$order = new Order(
    $customerId,
    $items,
    $paymentMethod,
    $shippingAddress,
    $discountCode,
    $giftWrap = true,
    $specialInstructions = null
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not only is it hard to read, but it’s also easy to make mistakes when creating orders. &lt;strong&gt;Because of this&lt;/strong&gt;, the Builder Pattern becomes a natural solution. It lets you construct objects step by step, handling optional parameters elegantly while keeping your code readable and maintainable.&lt;/p&gt;

&lt;p&gt;In this article, we’ll explore how to apply the Builder Pattern to an &lt;strong&gt;Order class in PHP/Laravel&lt;/strong&gt;, improving readability, testability, and flexibility.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/01/17/factory-method-in-php/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/01/17/factory-method-in-php/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Builder Pattern Works for Orders
&lt;/h2&gt;

&lt;p&gt;The Builder Pattern is especially useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A class has &lt;strong&gt;many optional parameters&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The class requires &lt;strong&gt;complex setup or validation&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;avoid long constructors&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You need &lt;strong&gt;clear, readable, fluent code&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In e-commerce, orders often fit all these criteria. By using a builder, we can separate the construction logic from the &lt;strong&gt;order behavior&lt;/strong&gt;, making it easier to extend and maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Designing the Order Class
&lt;/h2&gt;

&lt;p&gt;Let’s start with a simple Order class that represents a finalized order object. We’ll assume that once an order is created, it’s immutable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Order
{
    public function __construct(
        public int $customerId,
        public array $items,
        public string $paymentMethod,
        public string $shippingAddress,
        public ?string $discountCode = null,
        public bool $giftWrap = false,
        public ?string $specialInstructions = null
    ) {}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;However&lt;/strong&gt;, the constructor still takes multiple parameters, some of them optional. While this works, it doesn’t scale well if more options are added in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Creating the Order Builder
&lt;/h2&gt;

&lt;p&gt;We can create an &lt;strong&gt;OrderBuilder&lt;/strong&gt; class to handle object creation step by step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class OrderBuilder
{
    private int $customerId;
    private array $items = [];
    private string $paymentMethod = 'credit_card';
    private string $shippingAddress = '';
    private ?string $discountCode = null;
    private bool $giftWrap = false;
    private ?string $specialInstructions = null;

    public static function create(): self
    {
        return new self();
    }

    public function customerId(int $customerId): self
    {
        $this-&amp;gt;customerId = $customerId;
        return $this;
    }

    public function addItem(array $item): self
    {
        $this-&amp;gt;items[] = $item;
        return $this;
    }

    public function paymentMethod(string $method): self
    {
        $this-&amp;gt;paymentMethod = $method;
        return $this;
    }

    public function shippingAddress(string $address): self
    {
        $this-&amp;gt;shippingAddress = $address;
        return $this;
    }

    public function discountCode(string $code): self
    {
        $this-&amp;gt;discountCode = $code;
        return $this;
    }

    public function giftWrap(bool $flag): self
    {
        $this-&amp;gt;giftWrap = $flag;
        return $this;
    }

    public function specialInstructions(string $instructions): self
    {
        $this-&amp;gt;specialInstructions = $instructions;
        return $this;
    }

    public function build(): Order
    {
        if (empty($this-&amp;gt;customerId)) {
            throw new \InvalidArgumentException('Customer ID is required.');
        }

        if (empty($this-&amp;gt;items)) {
            throw new \InvalidArgumentException('At least one item is required.');
        }

        if (empty($this-&amp;gt;shippingAddress)) {
            throw new \InvalidArgumentException('Shipping address is required.');
        }

        return new Order(
            $this-&amp;gt;customerId,
            $this-&amp;gt;items,
            $this-&amp;gt;paymentMethod,
            $this-&amp;gt;shippingAddress,
            $this-&amp;gt;discountCode,
            $this-&amp;gt;giftWrap,
            $this-&amp;gt;specialInstructions
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice a few key things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fluent interface:&lt;/strong&gt; Each setter returns &lt;em&gt;$this&lt;/em&gt;, allowing chained calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation in build():&lt;/strong&gt; Mandatory fields are checked before creating the &lt;em&gt;Order&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stepwise construction:&lt;/strong&gt; You can add items or set options one by one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Using the Builder
&lt;/h2&gt;

&lt;p&gt;Here’s how creating an order looks &lt;strong&gt;without a builder&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$order = new Order(
    123,
    [['product_id' =&amp;gt; 1, 'quantity' =&amp;gt; 2], ['product_id' =&amp;gt; 5, 'quantity' =&amp;gt; 1]],
    'paypal',
    '123 Main Street, City, Country',
    'DISCOUNT10',
    true,
    'Leave at the front door'
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;As a result&lt;/strong&gt;, it’s hard to read and error-prone.&lt;/p&gt;

&lt;p&gt;With the builder, it becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$order = OrderBuilder::create()
    -&amp;gt;customerId(123)
    -&amp;gt;addItem(['product_id' =&amp;gt; 1, 'quantity' =&amp;gt; 2])
    -&amp;gt;addItem(['product_id' =&amp;gt; 5, 'quantity' =&amp;gt; 1])
    -&amp;gt;paymentMethod('paypal')
    -&amp;gt;shippingAddress('123 Main Street, City, Country')
    -&amp;gt;discountCode('DISCOUNT10')
    -&amp;gt;giftWrap(true)
    -&amp;gt;specialInstructions('Leave at the front door')
    -&amp;gt;build();

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Much cleaner and readable.&lt;/li&gt;
&lt;li&gt;Easy to skip optional fields or add more items.&lt;/li&gt;
&lt;li&gt;Validation ensures that you set all required fields.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Integrating with Laravel Jobs or Services
&lt;/h2&gt;

&lt;p&gt;In Laravel, orders are often &lt;strong&gt;processed asynchronously&lt;/strong&gt; using Jobs or Services. Builders make this easier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$order = OrderBuilder::create()
    -&amp;gt;customerId($user-&amp;gt;id)
    -&amp;gt;addItem(['product_id' =&amp;gt; $product-&amp;gt;id, 'quantity' =&amp;gt; 1])
    -&amp;gt;shippingAddress($user-&amp;gt;address)
    -&amp;gt;build();

dispatch(new ProcessOrderJob($order));

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also &lt;strong&gt;create reusable templates&lt;/strong&gt; for common order setups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class OrderBuilderFactory
{
    public static function giftOrder(int $customerId, array $items, string $address): OrderBuilder
    {
        return OrderBuilder::create()
            -&amp;gt;customerId($customerId)
            -&amp;gt;shippingAddress($address)
            -&amp;gt;giftWrap(true)
            -&amp;gt;paymentMethod('credit_card')
            -&amp;gt;addItem(...$items);
    }
}

$order = OrderBuilderFactory::giftOrder($user-&amp;gt;id, $items, $user-&amp;gt;address)-&amp;gt;build();

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is perfect when you have &lt;strong&gt;standardized order types&lt;/strong&gt;, like gifts or bulk orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Advantages of Using the Builder Pattern
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Readability&lt;/strong&gt;: The creation of orders reads like a series of instructions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validation&lt;/strong&gt;: All required fields are checked in one place.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: Optional fields can be skipped without affecting the rest.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testability&lt;/strong&gt;: Builders can be tested independently from the Order class.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Extensibility&lt;/strong&gt;: Adding new optional features (e.g., gift wrap, promo codes) doesn’t break existing code.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 6: When Not to Use a Builder
&lt;/h2&gt;

&lt;p&gt;You don’t always need builders. Avoid using them if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The class only has 2–3 parameters.&lt;/li&gt;
&lt;li&gt;Objects are simple DTOs without optional parameters.&lt;/li&gt;
&lt;li&gt;Fluent APIs add unnecessary complexity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For small objects, a simple constructor or named constructors may suffice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Summary
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Builder Pattern&lt;/strong&gt; is a simple yet powerful way to manage &lt;strong&gt;complex object creation&lt;/strong&gt; in PHP and Laravel. By separating construction from the object itself, your code becomes &lt;strong&gt;readable, maintainable, and testable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For an &lt;em&gt;Order&lt;/em&gt; class with multiple optional features, the builder provides a &lt;strong&gt;clear, step-by-step&lt;/strong&gt; way to create instances without dealing with confusing long constructors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the builder for &lt;strong&gt;complex or optional-heavy objects.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Keep &lt;strong&gt;validation in the build() method.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Leverage &lt;strong&gt;fluent interfaces&lt;/strong&gt; for readability.&lt;/li&gt;
&lt;li&gt;Consider &lt;strong&gt;factory methods for reusable templates&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the Builder Pattern, your order creation code can grow with your application—without turning into a mess of parameters and confusion.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>php</category>
      <category>laravel</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why "Almost Done" Work Breaks Development Flow</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 03 Feb 2026 14:57:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/why-almost-done-work-breaks-development-flow-274h</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/why-almost-done-work-breaks-development-flow-274h</guid>
      <description>&lt;p&gt;Every team has it.&lt;/p&gt;

&lt;p&gt;A pull request that's "basically finished."&lt;br&gt;
A feature that's "just missing a few small things."&lt;br&gt;
A task marked as almost done for the third week in a row.&lt;/p&gt;

&lt;p&gt;It rarely looks like a problem. There's no failing build, no red alerts, no production outage. And yet, this kind of work is one of the most effective ways to quietly destroy development flow.&lt;/p&gt;

&lt;p&gt;Not by being wrong - but by never being truly finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous article in this category:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://codecraftdiary.com/2026/01/10/effective-code-reviews/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/01/10/effective-code-reviews/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Almost Done" Really Means
&lt;/h2&gt;

&lt;p&gt;In theory, almost done sounds harmless. In practice, it usually means one or more of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests are missing or incomplete&lt;/li&gt;
&lt;li&gt;Edge cases are known but postponed&lt;/li&gt;
&lt;li&gt;Naming or API design "can be improved later"&lt;/li&gt;
&lt;li&gt;Migration exists, rollback does not&lt;/li&gt;
&lt;li&gt;Documentation is optional… for now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these issues are dramatic on their own. That's why they survive. The problem is that almost done work doesn't behave like work in progress - it behaves like invisible technical inventory.&lt;/p&gt;

&lt;p&gt;It sits in the system.&lt;br&gt;
It consumes attention.&lt;br&gt;
And it blocks flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Illusion of Progress
&lt;/h2&gt;

&lt;p&gt;Half-finished work creates a powerful illusion: movement without &lt;strong&gt;completion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From the outside, things look active:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PRs are open&lt;/li&gt;
&lt;li&gt;Commits keep coming&lt;/li&gt;
&lt;li&gt;Sprint boards show progress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But internally, the system is stuck.&lt;/p&gt;

&lt;p&gt;Developers context-switch back to old code.&lt;br&gt;
Reviewers repeatedly re-load the same mental model.&lt;br&gt;
Small unresolved decisions accumulate into cognitive debt.&lt;/p&gt;

&lt;p&gt;Nothing moves forward cleanly, because nothing is allowed to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It's Worse Than Doing Nothing
&lt;/h2&gt;

&lt;p&gt;A task that hasn't been started yet is cheap.&lt;/p&gt;

&lt;p&gt;A task that's almost done is expensive.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because almost done work demands continuous re-entry. Every return requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rebuilding context&lt;/li&gt;
&lt;li&gt;Re-evaluating past decisions&lt;/li&gt;
&lt;li&gt;Re-answering questions that should have been resolved once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not linear cost - it compounds.&lt;/p&gt;

&lt;p&gt;The longer work stays in this state, the more expensive finishing it becomes. At some point, teams stop finishing at all. They work around it instead.&lt;/p&gt;

&lt;p&gt;That's when flow collapses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review Fatigue: The Silent Multiplier
&lt;/h2&gt;

&lt;p&gt;Code reviews amplify this problem dramatically.&lt;/p&gt;

&lt;p&gt;A PR that's nearly ready but not quite there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gets reviewed&lt;/li&gt;
&lt;li&gt;Receives feedback&lt;/li&gt;
&lt;li&gt;Gets partially updated&lt;/li&gt;
&lt;li&gt;Then stalls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When it comes back days later, the reviewer must start over.&lt;/p&gt;

&lt;p&gt;Multiply that by several PRs and several developers, and you get review fatigue - not because reviews are bad, but because they never conclude.&lt;/p&gt;

&lt;p&gt;At that point, reviewers stop caring about quality and focus on getting it merged. Standards drop, resentment grows, and nobody can quite explain why.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Pull Requests Wait, Conflicts Multiply
&lt;/h2&gt;

&lt;p&gt;One of the most visible symptoms of almost done work is a pull request that sits open for too long.&lt;/p&gt;

&lt;p&gt;At first, nothing seems wrong. The code is there. CI is green. Review is "scheduled."&lt;br&gt;
Then time passes.&lt;/p&gt;

&lt;p&gt;While the PR waits, the main branch moves on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New features are merged&lt;/li&gt;
&lt;li&gt;Refactors happen elsewhere&lt;/li&gt;
&lt;li&gt;Dependencies shift subtly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the original author finally comes back, the PR is no longer just waiting for approval - it's out of date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: The PR That Was "Basically Ready"
&lt;/h2&gt;

&lt;p&gt;Imagine a backend pull request that introduces a new endpoint.&lt;/p&gt;

&lt;p&gt;The logic is implemented. Tests exist, but only for the happy path.&lt;br&gt;
The author opens the PR and asks for review.&lt;/p&gt;

&lt;p&gt;The review doesn't happen that day.&lt;/p&gt;

&lt;p&gt;Two days later, another feature touches the same service.&lt;br&gt;
A small refactor is merged. Nothing dramatic.&lt;/p&gt;

&lt;p&gt;When the original PR finally gets reviewed, it now has conflicts.&lt;br&gt;
Tests fail because assumptions changed. The reviewer's comments are no longer fully relevant.&lt;/p&gt;

&lt;p&gt;What was a one-hour review becomes a half-day cleanup.&lt;/p&gt;

&lt;p&gt;Nothing about the code was wrong.&lt;br&gt;
It just waited long enough for the system to move around it.&lt;/p&gt;

&lt;p&gt;That's the real cost of "basically finished" work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merge Conflicts Are Not a Git Problem
&lt;/h2&gt;

&lt;p&gt;At this point, conflicts appear. And teams often treat them as a tooling issue.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;p&gt;Merge conflicts are a &lt;strong&gt;process failure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They happen because work was allowed to remain unfinished long enough for the system around it to change. The longer a PR stays open, the higher the probability that it collides with unrelated changes.&lt;/p&gt;

&lt;p&gt;This creates a feedback loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR waits for review&lt;/li&gt;
&lt;li&gt;Branch diverges&lt;/li&gt;
&lt;li&gt;Conflicts appear&lt;/li&gt;
&lt;li&gt;Author postpones fixing them&lt;/li&gt;
&lt;li&gt;Review is delayed again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PR becomes heavier every day it remains open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Psychological Cost of Rebase Hell
&lt;/h2&gt;

&lt;p&gt;Long-lived PRs don't just create technical conflicts - they create psychological resistance.&lt;/p&gt;

&lt;p&gt;Developers start associating the PR with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Painful rebases&lt;/li&gt;
&lt;li&gt;Repeated test failures&lt;/li&gt;
&lt;li&gt;Comments they've already addressed once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So they avoid it.&lt;/p&gt;

&lt;p&gt;"This will take a full afternoon."&lt;br&gt;
"I'll deal with it when I have time."&lt;/p&gt;

&lt;p&gt;That avoidance keeps the work in the almost done state even longer - exactly where it does the most damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review Latency Is the Real Bottleneck
&lt;/h2&gt;

&lt;p&gt;Teams often blame developers for not finishing work.&lt;/p&gt;

&lt;p&gt;But in many cases, the real issue is review latency.&lt;/p&gt;

&lt;p&gt;If reviews routinely take days:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers stop prioritizing clean completion&lt;/li&gt;
&lt;li&gt;PR size increases ("If it takes days anyway…")&lt;/li&gt;
&lt;li&gt;Conflicts become normal, not exceptional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the workflow trains people to leave things unfinished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short-Lived PRs, Short-Lived Problems
&lt;/h2&gt;

&lt;p&gt;The solution is not hero reviewers or stricter rules. &lt;br&gt;
It's shorter lifetimes.&lt;/p&gt;

&lt;p&gt;A PR that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is reviewed within hours&lt;/li&gt;
&lt;li&gt;Merged the same day&lt;/li&gt;
&lt;li&gt;Or explicitly rejected early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…does not accumulate conflicts, fatigue, or resentment.&lt;/p&gt;

&lt;p&gt;It either finishes - or it dies quickly.&lt;/p&gt;

&lt;p&gt;Both outcomes preserve flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make Waiting Visible&lt;/strong&gt;&lt;br&gt;
One simple but effective practice is to treat waiting PRs as first-class problems.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Highlight PRs open longer than 48 hours&lt;/li&gt;
&lt;li&gt;Treat "waiting for review" as a blocker, not a state&lt;/li&gt;
&lt;li&gt;Prefer reviewing over starting new work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When waiting becomes visible, it stops being normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flow Breaks Where Work Waits
&lt;/h2&gt;

&lt;p&gt;Conflicts, rebases, and stalled PRs are not isolated issues.&lt;/p&gt;

&lt;p&gt;They are downstream effects of allowing work to stay almost done.&lt;/p&gt;

&lt;p&gt;If you want fewer conflicts, don't fight Git.&lt;br&gt;
Finish work faster - or stop starting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lean Has a Name for This
&lt;/h2&gt;

&lt;p&gt;In Lean thinking, unfinished work is called inventory.&lt;/p&gt;

&lt;p&gt;Inventory is dangerous because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It hides problems&lt;/li&gt;
&lt;li&gt;It ties up resources&lt;/li&gt;
&lt;li&gt;It creates false confidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Software teams rarely think in these terms, but the parallel is exact.&lt;/p&gt;

&lt;p&gt;Half-finished features are inventory.&lt;br&gt;
Open PRs are inventory.&lt;br&gt;
"Almost done" tasks are inventory.&lt;/p&gt;

&lt;p&gt;And like any excess inventory, they slow the entire system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Causes (That Sound Reasonable)
&lt;/h2&gt;

&lt;p&gt;The most dangerous causes are the ones that feel justified:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Large Pull Requests&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;"Let's do it properly in one go."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Large PRs increase the chance that something remains unresolved - and once they stall, they tend to stay stalled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Vague Definition of Done&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;em&gt;done&lt;/em&gt; isn't explicit, almost done becomes acceptable.&lt;/p&gt;

&lt;p&gt;Without a shared baseline (tests, docs, rollback, review passed), everyone uses their own internal standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deferred Decisions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"We'll clean this up later."&lt;br&gt;
"Let's just ship it first."&lt;br&gt;
Later rarely comes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Backend Teams Feel This More
&lt;/h2&gt;

&lt;p&gt;Backend work has properties that make almost done especially toxic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hidden coupling between systems&lt;/li&gt;
&lt;li&gt;Migrations that can't be half-applied&lt;/li&gt;
&lt;li&gt;APIs that lock in contracts early&lt;/li&gt;
&lt;li&gt;Bugs that surface weeks later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A frontend workaround might be ugly but visible.&lt;br&gt;
A backend shortcut is often invisible - until it explodes.&lt;/p&gt;

&lt;p&gt;That's why backend teams feel "slow" even when they work constantly.&lt;/p&gt;

&lt;p&gt;They're not slow. They're stuck finishing yesterday's almost-done work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Fixes That Actually Help
&lt;/h2&gt;

&lt;p&gt;This is not about discipline or motivation. It's about system design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Define "Done" Ruthlessly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If it's merged, it must include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests&lt;/li&gt;
&lt;li&gt;Review approval&lt;/li&gt;
&lt;li&gt;Clear naming&lt;/li&gt;
&lt;li&gt;Migration safety (if applicable)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything else is not done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Reduce PR Size&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Smaller PRs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finish faster&lt;/li&gt;
&lt;li&gt;Get reviewed faster&lt;/li&gt;
&lt;li&gt;Fail faster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They either complete - or get killed early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Ban "Almost Done" Language&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This sounds extreme, but language shapes behavior.&lt;br&gt;
Replace:&lt;br&gt;
&lt;em&gt;"It's almost done"&lt;/em&gt;&lt;br&gt;
With:&lt;br&gt;
&lt;em&gt;"It's blocked because X"&lt;/em&gt;&lt;br&gt;
Blocked work is honest. Almost done is deceptive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Prefer Completion Over Throughput&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Shipping fewer things that actually finish beats starting many things that don't.&lt;/p&gt;

&lt;p&gt;Flow comes from completion, not activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finishing Is a Skill
&lt;/h2&gt;

&lt;p&gt;Many teams optimize for starting work.&lt;br&gt;
Few optimize for finishing it.&lt;/p&gt;

&lt;p&gt;Finishing requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Making decisions uncomfortable early&lt;/li&gt;
&lt;li&gt;Accepting "good enough" instead of perfect&lt;/li&gt;
&lt;li&gt;Closing loops deliberately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It feels slower at first.&lt;br&gt;
Then everything accelerates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Flow Is About Endings
&lt;/h2&gt;

&lt;p&gt;Development flow isn't created by speed, tools, or frameworks.&lt;/p&gt;

&lt;p&gt;It's created by &lt;strong&gt;ending things cleanly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your team feels busy but stuck, don't ask how to move faster.&lt;br&gt;
Ask how much work is almost done - and why it's allowed to stay that way.&lt;/p&gt;

&lt;p&gt;Because unfinished work doesn't just wait.&lt;/p&gt;

&lt;p&gt;It actively slows everything around it.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>software</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Mocking External APIs in PHP (Sandbox Example)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 27 Jan 2026 14:18:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/mocking-external-apis-in-php-sandbox-example-5312</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/mocking-external-apis-in-php-sandbox-example-5312</guid>
      <description>&lt;p&gt;Testing external API integrations thoroughly is one of the hardest parts of building reliable web applications. Live API calls are slow, flaky, costly, and can fail unpredictably due to network issues, rate limits, or provider outages. For these reasons, most mature test suites do not make real HTTP requests — they mock them.&lt;/p&gt;

&lt;p&gt;This article walks you through mocking external HTTP APIs in plain PHP, from basics to advanced techniques, with sandbox-style examples you can run immediately. We’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why mocking is essential&lt;/li&gt;
&lt;li&gt;Creating a PHP API client&lt;/li&gt;
&lt;li&gt;Mocking responses with Guzzle&lt;/li&gt;
&lt;li&gt;Dynamic and conditional fake responses&lt;/li&gt;
&lt;li&gt;Simulating error conditions (timeouts, server errors)&lt;/li&gt;
&lt;li&gt;Asserting requests were made correctly&lt;/li&gt;
&lt;li&gt;Sequential responses for testing retries&lt;/li&gt;
&lt;li&gt;Using sandbox APIs for integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Previous article in testing category: &lt;a href="https://codecraftdiary.com/2026/01/03/testing-database-logic-what-to-test-what-to-skip-and-why-it-matters/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/01/03/testing-database-logic-what-to-test-what-to-skip-and-why-it-matters/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why Mock External API Calls
&lt;/h2&gt;

&lt;p&gt;Before diving into code, here’s why mocking is crucial:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a. Speed&lt;/strong&gt;&lt;br&gt;
Live HTTP calls dramatically slow down tests — often orders of magnitude slower than in-memory logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;b. Stability&lt;/strong&gt;&lt;br&gt;
Third-party APIs can be unreliable, returning downtime or throttling, which makes tests flaky.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;c. Isolation&lt;/strong&gt;&lt;br&gt;
Tests should verify your application logic, not the availability or correctness of external services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;d. Cost and Limits&lt;/strong&gt;&lt;br&gt;
Calling paid APIs every test run can incur costs or hit rate limits.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Creating a PHP HTTP Client
&lt;/h2&gt;

&lt;p&gt;We’ll use Guzzle as our HTTP client. Here’s a simple &lt;em&gt;WeatherClient&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class WeatherClient
{
    private Client $client;
    private string $baseUrl;
    private string $apiKey;

    public function __construct(string $baseUrl, string $apiKey)
    {
        $this-&amp;gt;baseUrl = $baseUrl;
        $this-&amp;gt;apiKey = $apiKey;
        $this-&amp;gt;client = new Client(['base_uri' =&amp;gt; $baseUrl]);
    }

    public function getCurrent(string $city): array
    {
        try {
            $response = $this-&amp;gt;client-&amp;gt;request('GET', '/current', [
                'query' =&amp;gt; ['q' =&amp;gt; $city, 'key' =&amp;gt; $this-&amp;gt;apiKey],
                'http_errors' =&amp;gt; false
            ]);
            return json_decode($response-&amp;gt;getBody()-&amp;gt;getContents(), true);
        } catch (RequestException $e) {
            throw new \RuntimeException("API request failed: ".$e-&amp;gt;getMessage());
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;GuzzleHttp\Client&lt;/em&gt; handles HTTP requests&lt;/li&gt;
&lt;li&gt;Works in plain PHP without Laravel&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Mocking API Responses with Guzzle
&lt;/h2&gt;

&lt;p&gt;Guzzle provides &lt;strong&gt;MockHandler&lt;/strong&gt; to simulate responses. Here’s a basic test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;

// Mock API response
$mock = new MockHandler([
    new Response(200, [], json_encode([
        'location' =&amp;gt; ['name' =&amp;gt; 'London'],
        'current' =&amp;gt; ['temp_c' =&amp;gt; 18]
    ]))
]);

$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' =&amp;gt; $handlerStack, 'base_uri' =&amp;gt; 'https://api.weather.com']);

// Inject mock client into WeatherClient
$weatherClient = new WeatherClient('https://api.weather.com', 'dummy-key');
$ref = new ReflectionClass($weatherClient);
$prop = $ref-&amp;gt;getProperty('client');
$prop-&amp;gt;setAccessible(true);
$prop-&amp;gt;setValue($weatherClient, $client);

// Make request (uses mocked response)
$result = $weatherClient-&amp;gt;getCurrent('London');

echo "City: ".$result['location']['name']."\n";   // London
echo "Temperature: ".$result['current']['temp_c']."°C\n"; // 18

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;No real HTTP requests are made&lt;/li&gt;
&lt;li&gt;Ideal for unit testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3a. Basic Example: Mocking a Simple API in Plain PHP Sandbox
&lt;/h2&gt;

&lt;p&gt;This version shows how to simulate external API responses in plain PHP, without Guzzle or Laravel, making it suitable for online sandbox environments like &lt;a href="https://onlinephp.io/c/d4940" rel="noopener noreferrer"&gt;https://onlinephp.io/c/d4940&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php
// Simple WeatherClient that returns fake responses
class WeatherClient
{
    private $baseUrl;
    private $apiKey;

    public function __construct(string $baseUrl, string $apiKey)
    {
        $this-&amp;gt;baseUrl = $baseUrl;
        $this-&amp;gt;apiKey = $apiKey;
    }

    // Fake API call returning predefined data
    public function getCurrent(string $city): array
    {
        $fakeApiResponses = [
            'London' =&amp;gt; [
                'location' =&amp;gt; ['name' =&amp;gt; 'London'],
                'current' =&amp;gt; ['temp_c' =&amp;gt; 18, 'condition' =&amp;gt; 'Partly cloudy']
            ],
            'New York' =&amp;gt; [
                'location' =&amp;gt; ['name' =&amp;gt; 'New York'],
                'current' =&amp;gt; ['temp_c' =&amp;gt; 22, 'condition' =&amp;gt; 'Sunny']
            ]
        ];

        return $fakeApiResponses[$city] ?? [
            'location' =&amp;gt; ['name' =&amp;gt; $city],
            'current' =&amp;gt; ['temp_c' =&amp;gt; null, 'condition' =&amp;gt; 'Unknown']
        ];
    }
}

// --- Sandbox Test ---
$client = new WeatherClient('https://api.fakeweather.com', 'dummy-key');

$cities = ['London', 'New York', 'Paris'];

foreach ($cities as $city) {
    $result = $client-&amp;gt;getCurrent($city);
    $temp = $result['current']['temp_c'] ?? 'unknown';
    $condition = $result['current']['condition'] ?? 'unknown';
    echo "City: {$result['location']['name']}, Temperature: {$temp}°C, Condition: {$condition}\n";
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why include this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Shows a &lt;strong&gt;fully runnable example&lt;/strong&gt; in any PHP environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Illustrates the concept of &lt;strong&gt;mocking/faking API responses&lt;/strong&gt; without external dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Complements the Guzzle-based example for readers who want a &lt;strong&gt;quick sandbox-ready approach&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Dynamic and Conditional Fake Responses
&lt;/h2&gt;

&lt;p&gt;You can customize responses based on input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$mock = new MockHandler([
    function ($request, $options) {
        parse_str($request-&amp;gt;getUri()-&amp;gt;getQuery(), $query);
        if ($query['q'] === 'US') {
            return new Response(200, [], json_encode(['data'=&amp;gt;'US result']));
        }
        return new Response(200, [], json_encode(['data'=&amp;gt;'Other result']));
    }
]);

$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' =&amp;gt; $handlerStack]);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Use closures to decide response dynamically&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Simulating Errors: Timeouts &amp;amp; Server Failures
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;a. Failed Connection / Timeout&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$mock = new MockHandler([
    new \GuzzleHttp\Exception\ConnectException(
        "Connection failed",
        new \GuzzleHttp\Psr7\Request('GET', '/current')
    )
]);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;b. Server Errors (HTTP 500)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$mock = new MockHandler([
    new Response(500, [], 'Internal Server Error')
]);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;c. Sequential Responses for Retry Logic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$mock = new MockHandler([
    new Response(500, [], 'Fail'),  // First request
    new Response(200, [], json_encode(['location'=&amp;gt;['name'=&amp;gt;'London'], 'current'=&amp;gt;['temp_c'=&amp;gt;20]])), // Second
]);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Useful for testing retry behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Assertions
&lt;/h2&gt;

&lt;p&gt;In PHPUnit, you can assert the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$this-&amp;gt;assertEquals('London', $result['location']['name']);
$this-&amp;gt;assertEquals(18, $result['current']['temp_c']);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a sandbox without PHPUnit, simple checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if ($result['location']['name'] === 'London') {
    echo "Test passed!\n";
} else {
    echo "Test failed!\n";
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Using Fixtures for Realistic Responses
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$body = file_get_contents('weather_fixture.json');
$mock = new MockHandler([ new Response(200, [], $body) ]);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Easier to maintain and mirrors real API structure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Integration Testing with Sandbox APIs
&lt;/h2&gt;

&lt;p&gt;Some APIs provide &lt;strong&gt;sandbox environments&lt;/strong&gt; (Stripe, PayPal, GitHub) where you can test real HTTP requests safely.&lt;/p&gt;

&lt;p&gt;For full sandbox testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configure your client’s base URL to the sandbox&lt;/li&gt;
&lt;li&gt;Optionally, use &lt;strong&gt;Wiremock&lt;/strong&gt; or similar mock servers for advanced stubbing&lt;/li&gt;
&lt;li&gt;Run integration tests against sandbox to ensure real API compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Mock &lt;strong&gt;external APIs only&lt;/strong&gt;, don’t mock internal logic&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;descriptive fixtures&lt;/strong&gt; for clarity&lt;/li&gt;
&lt;li&gt;Include &lt;strong&gt;error scenarios&lt;/strong&gt; in tests (timeouts, 500 errors)&lt;/li&gt;
&lt;li&gt;Combine &lt;strong&gt;unit tests (mocked)&lt;/strong&gt; with occasional &lt;strong&gt;sandbox integration tests&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>software</category>
      <category>php</category>
    </item>
    <item>
      <title>Factory Method in PHP: When Refactoring Leads to a Pattern</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 20 Jan 2026 15:09:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/factory-method-in-php-when-refactoring-leads-to-a-pattern-3hho</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/factory-method-in-php-when-refactoring-leads-to-a-pattern-3hho</guid>
      <description>&lt;p&gt;Factory Method is one of those patterns that many PHP developers encounter early, but fully understand much later. It is often introduced as a “creational pattern” with diagrams and inheritance hierarchies, yet in real projects it rarely starts that way.&lt;/p&gt;

&lt;p&gt;In practice, Factory Method is almost never a design decision made upfront. It is a &lt;strong&gt;destination reached through refactoring—usually&lt;/strong&gt; when simple object creation starts to interfere with otherwise stable business logic.&lt;/p&gt;

&lt;p&gt;This article explains &lt;strong&gt;how Factory Method emerges naturally&lt;/strong&gt;, what problems it actually solves, and where developers frequently misuse it.&lt;/p&gt;

&lt;p&gt;Factory Method is often described as the “younger sibling” of Abstract Factory.&lt;/p&gt;

&lt;p&gt;Here you can find article about Abstract factory: &lt;a href="https://codecraftdiary.com/2025/12/07/abstract-factory-pattern-php/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2025/12/07/abstract-factory-pattern-php/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Starting Point: Simple Object Creation
&lt;/h2&gt;

&lt;p&gt;Every PHP project begins with direct instantiation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$exporter = new CsvExporter();
$exporter-&amp;gt;export($data);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is clean, readable, and perfectly correct.&lt;br&gt;
No abstraction is needed because there is no variability yet.&lt;/p&gt;

&lt;p&gt;The mistake many developers make is assuming that patterns should exist before problems do. In reality, the absence of patterns here is a sign of healthy code.&lt;/p&gt;
&lt;h2&gt;
  
  
  The First Smell: Conditional Creation
&lt;/h2&gt;

&lt;p&gt;As requirements grow, object creation often becomes conditional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if ($format === 'csv') {
    $exporter = new CsvExporter();
} elseif ($format === 'xlsx') {
    $exporter = new XlsxExporter();
} else {
    throw new RuntimeException('Unsupported format');
}

$exporter-&amp;gt;export($data);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code still works, but several problems appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business logic is mixed with creation logic&lt;/li&gt;
&lt;li&gt;Adding a new exporter requires modifying this method&lt;/li&gt;
&lt;li&gt;The method now has multiple reasons to change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, this situation &lt;strong&gt;still does not justify Factory Method&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At this stage, the problem is not patterns—it is responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Correct First Move: Extract Creation
&lt;/h2&gt;

&lt;p&gt;The right response is not introducing a GoF pattern, but performing a simple refactoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;protected function createExporter(string $format): Exporter
{
    if ($format === 'csv') {
        return new CsvExporter();
    }

    if ($format === 'xlsx') {
        return new XlsxExporter();
    }

    throw new RuntimeException('Unsupported format');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step is critical. It separates what the code does from what it creates.&lt;/p&gt;

&lt;p&gt;At this point, many developers stop—and often they should. If object creation remains stable and parameter-driven, a Strategy, a map, or even a simple switch is usually enough.&lt;/p&gt;

&lt;p&gt;Factory Method only becomes relevant if &lt;strong&gt;creation itself must vary by context.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Key Question
&lt;/h2&gt;

&lt;p&gt;Before going any further, ask:&lt;/p&gt;

&lt;p&gt;Will different variants of this class need to create &lt;strong&gt;different&lt;/strong&gt; implementations, while the overall algorithm stays the same?&lt;/p&gt;

&lt;p&gt;If the answer is &lt;strong&gt;no&lt;/strong&gt;, Factory Method is the wrong tool.&lt;/p&gt;

&lt;p&gt;If the answer is &lt;strong&gt;yes&lt;/strong&gt;, Factory Method is likely the simplest correct solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Pattern Emerges
&lt;/h2&gt;

&lt;p&gt;Consider a scenario where you now have multiple export services, each following the same workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate data&lt;/li&gt;
&lt;li&gt;Create exporter&lt;/li&gt;
&lt;li&gt;Export&lt;/li&gt;
&lt;li&gt;Log result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow is identical. Only the exporter differs.&lt;/p&gt;

&lt;p&gt;This is where Factory Method naturally appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the Stable Algorithm
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;abstract class ExportService
{
    public function export(array $data): void
    {
        $this-&amp;gt;validate($data);

        $exporter = $this-&amp;gt;createExporter();
        $exporter-&amp;gt;export($data);

        $this-&amp;gt;log();
    }

    abstract protected function createExporter(): Exporter;

    protected function validate(array $data): void
    {
        // shared validation logic
    }

    protected function log(): void
    {
        // shared logging logic
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;algorithm is fixed&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;creation step is deferred&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Subclasses decide what to instantiate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is Factory Method—not because the pattern was chosen, but because the structure demanded it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concrete Implementations
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class CsvExportService extends ExportService
{
    protected function createExporter(): Exporter
    {
        return new CsvExporter();
    }
}

class XlsxExportService extends ExportService
{
    protected function createExporter(): Exporter
    {
        return new XlsxExporter();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each subclass controls creation, while the base class controls behavior.&lt;/p&gt;

&lt;p&gt;This separation is the essence of Factory Method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works Better Than Conditionals
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Stable Core Logic&lt;/strong&gt;&lt;br&gt;
The export process is written once. It does not care which exporter exists, only that one exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Open for Extension&lt;/strong&gt;&lt;br&gt;
New exporters require new subclasses, not changes to existing logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Improved Testability&lt;/strong&gt;&lt;br&gt;
You can override &lt;em&gt;createExporter()&lt;/em&gt; in tests and inject test doubles without touching production code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Misunderstandings
&lt;/h2&gt;

&lt;p&gt;“Factory Method replaces conditionals”&lt;br&gt;
It does not.&lt;br&gt;
If the choice depends on runtime data, Factory Method is usually the wrong abstraction.&lt;/p&gt;

&lt;p&gt;“Factory Method is just a factory class”&lt;br&gt;
Factory Method is about inheritance and variation, not about standalone factory objects.&lt;/p&gt;

&lt;p&gt;“It should be public”&lt;br&gt;
In almost all cases, the factory method should be protected. Making it public leaks internal construction details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Factory Method vs Other Options
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedyi9ucrt1kgwmd1mcke.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedyi9ucrt1kgwmd1mcke.png" alt=" " width="752" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Factory Method sits in the middle: more flexible than direct &lt;br&gt;
instantiation, simpler than full factory hierarchies.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Use Factory Method
&lt;/h2&gt;

&lt;p&gt;Avoid Factory Method if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is only one concrete implementation&lt;/li&gt;
&lt;li&gt;Object creation depends purely on input values&lt;/li&gt;
&lt;li&gt;You need to assemble complex object graphs&lt;/li&gt;
&lt;li&gt;A simple data-driven approach is enough&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Patterns add structure—but also weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Factory Method is not something you “apply”.&lt;br&gt;
It is something your code &lt;strong&gt;grows into.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you extract object creation, notice that subclasses need to customize it, and want to keep the main algorithm untouched—you are already there.&lt;/p&gt;

&lt;p&gt;Used this way, Factory Method becomes one of the most practical and low-risk patterns in everyday PHP development.&lt;/p&gt;

&lt;p&gt;Not because it is clever—but because it respects how software actually evolves.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>refactorit</category>
      <category>php</category>
      <category>software</category>
    </item>
    <item>
      <title>Why Code Reviews Break Down in Backend Development - CodeCraft Diary</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Wed, 14 Jan 2026 14:30:00 +0000</pubDate>
      <link>https://forem.com/codecraft_diary_3d13677fb/why-code-reviews-break-down-in-backend-development-codecraft-diary-2mm3</link>
      <guid>https://forem.com/codecraft_diary_3d13677fb/why-code-reviews-break-down-in-backend-development-codecraft-diary-2mm3</guid>
      <description>&lt;p&gt;Almost every development team claims to do code reviews.&lt;br&gt;
Far fewer can honestly say their reviews consistently improve code quality and developer productivity.&lt;/p&gt;

&lt;p&gt;In many teams, code review slowly turns into a source of frustration: pull requests pile up, feedback is inconsistent, and developers either argue over details or stop caring altogether. The problem is rarely the people involved. It is almost always the &lt;strong&gt;design of the review process itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article looks at the most &lt;strong&gt;common code review problems teams face in real projects&lt;/strong&gt; and offers &lt;strong&gt;practical solutions that work in modern workflows.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Previous article: &lt;a href="https://codecraftdiary.com/2025/12/20/development-workflow-failures/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2025/12/20/development-workflow-failures/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: Code Reviews Try to Catch Everything
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Happens
&lt;/h2&gt;

&lt;p&gt;Many teams implicitly expect code review to be the final quality gate. Reviewers are supposed to catch logic bugs, style issues, missing tests, edge cases, performance problems, and architectural flaws - all at once.&lt;/p&gt;

&lt;p&gt;The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reviews take too long&lt;/li&gt;
&lt;li&gt;reviewers feel overwhelmed&lt;/li&gt;
&lt;li&gt;authors wait days for feedback&lt;/li&gt;
&lt;li&gt;reviews become superficial under time pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Fails
&lt;/h2&gt;

&lt;p&gt;Humans are bad at repetitive, mechanical checks. When reviewers are overloaded, they either miss important issues or focus on trivial ones.&lt;br&gt;
Expecting reviewers to act as both automation and architects guarantees burnout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A typical pull request in such teams looks like this:&lt;br&gt;
The CI pipeline is minimal or missing, so reviewers leave comments about formatting, unused imports, naming conventions, and missing null checks. By the time anyone looks at the actual behavior of the code, the discussion thread already has 20 comments.&lt;br&gt;
The same change with proper automation looks very different. Formatting, linting, and tests are enforced automatically. Review comments focus on edge cases, error handling, and whether the change actually solves the problem it claims to solve.&lt;br&gt;
Nothing about the developers changed. Only the responsibility of the review did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Narrow the Responsibility of Code Review
&lt;/h2&gt;

&lt;p&gt;A healthy process clearly defines what code review is not responsible for.&lt;/p&gt;

&lt;p&gt;Anything that can be checked automatically should never be reviewed manually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;formatting and style&lt;/li&gt;
&lt;li&gt;linting and static analysis&lt;/li&gt;
&lt;li&gt;basic type or syntax errors&lt;/li&gt;
&lt;li&gt;test execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once automation enforces the baseline, reviewers can focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;correctness of behavior&lt;/li&gt;
&lt;li&gt;clarity of intent&lt;/li&gt;
&lt;li&gt;risk and edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code review should reduce risk - not eliminate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: Reviews Focus on Opinions Instead of Outcomes
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Happens
&lt;/h2&gt;

&lt;p&gt;Comments like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I would write this differently"&lt;/li&gt;
&lt;li&gt;"This doesn't feel clean"&lt;/li&gt;
&lt;li&gt;"I don't like this abstraction"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Opinion-based feedback often sounds like this:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"I would structure this differently."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Outcome-based feedback reframes the same concern:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"This structure makes it hard to see how errors propagate. How would we diagnose a failure here in production?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Reviews turn into subjective debates. Decisions depend on who reviews the PR rather than on shared standards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Fails
&lt;/h2&gt;

&lt;p&gt;Opinion-driven reviews scale poorly. As teams grow, inconsistency increases and trust erodes. Authors stop learning because feedback feels arbitrary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Anchor Reviews to Explicit Criteria
&lt;/h2&gt;

&lt;p&gt;Effective reviews are guided by &lt;strong&gt;shared questions&lt;/strong&gt;, not personal taste.&lt;/p&gt;

&lt;p&gt;Examples of outcome-based review questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the intent of this change clear from the code and description?&lt;/li&gt;
&lt;li&gt;Does this implementation handle failure cases explicitly?&lt;/li&gt;
&lt;li&gt;Does this introduce hidden coupling or long-term maintenance risk?&lt;/li&gt;
&lt;li&gt;Is the complexity justified by the problem being solved?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When feedback is tied to outcomes, disagreements become productive discussions instead of personal conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: Pull Requests Are Too Large
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Happens
&lt;/h2&gt;

&lt;p&gt;Large pull requests accumulate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple features&lt;/li&gt;
&lt;li&gt;refactors mixed with behavior changes&lt;/li&gt;
&lt;li&gt;unrelated fixes "while I was there"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reviewers skim. Important issues get missed. Feedback arrives late or not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Fails
&lt;/h2&gt;

&lt;p&gt;Large PRs increase cognitive load. Reviewers cannot hold the full context in their heads, especially under time pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A single pull request includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a new feature&lt;/li&gt;
&lt;li&gt;a refactor of existing logic&lt;/li&gt;
&lt;li&gt;renamed variables across multiple files&lt;/li&gt;
&lt;li&gt;a few unrelated fixes "while touching the code"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reviewers now have to understand what changed and why, across multiple concerns. Important issues are missed simply because the mental load is too high.&lt;/p&gt;

&lt;p&gt;When the same work is split into three focused pull requests, reviews become faster, feedback is clearer, and risk is reduced - even though the total amount of code is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Enforce Small, Focused Changes
&lt;/h2&gt;

&lt;p&gt;A good rule of thumb:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If a reviewer cannot understand the purpose of a PR in five minutes, it is too large.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Practical improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate refactors from functional changes&lt;/li&gt;
&lt;li&gt;ship incremental improvements behind feature toggles if needed&lt;/li&gt;
&lt;li&gt;encourage early, partial PRs for feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small PRs are not about discipline. They are about enabling fast feedback loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 4: Reviews Ignore Failure Scenarios
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Happens
&lt;/h2&gt;

&lt;p&gt;Reviews focus on the happy path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;valid input&lt;/li&gt;
&lt;li&gt;successful API calls&lt;/li&gt;
&lt;li&gt;ideal system state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Edge cases and failure modes are rarely discussed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Fails
&lt;/h2&gt;

&lt;p&gt;Most production incidents are caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unexpected input&lt;/li&gt;
&lt;li&gt;partial failures&lt;/li&gt;
&lt;li&gt;timeouts and retries&lt;/li&gt;
&lt;li&gt;concurrency issues
If these are not considered during review, they will surface later - in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A change integrates with an external API. The review verifies that the happy path works and approves the pull request.&lt;/p&gt;

&lt;p&gt;In production, the API occasionally times out. The retry logic triggers twice, causing the same operation to run multiple times. Duplicate records appear. The issue is not a bug in the implementation - it is a missing discussion about failure modes during review.&lt;/p&gt;

&lt;p&gt;Asking "What happens if this runs twice?" during review would have surfaced the problem early.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Make Failure a First-Class Review Topic
&lt;/h2&gt;

&lt;p&gt;Reviewers should explicitly ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens if this dependency fails?&lt;/li&gt;
&lt;li&gt;What happens if this runs twice?&lt;/li&gt;
&lt;li&gt;What happens under load?&lt;/li&gt;
&lt;li&gt;What assumptions does this code rely on?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if not all cases are handled immediately, acknowledging them dramatically reduces future surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 5: Tests Exist but Don't Prove Anything Important
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Happens
&lt;/h2&gt;

&lt;p&gt;Tests are present, but they:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;assert implementation details&lt;/li&gt;
&lt;li&gt;mirror the code instead of validating behavior&lt;/li&gt;
&lt;li&gt;break frequently during refactors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reviews approve tests because "there are tests", not because they add confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Fails
&lt;/h2&gt;

&lt;p&gt;Decorative tests create a false sense of security. They increase maintenance cost without reducing risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Review Tests as Risk Protection
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A test asserts that a private method is called with specific parameters. The test passes, but it does not verify any observable behavior.&lt;/p&gt;

&lt;p&gt;After a refactor, the test breaks even though the system still behaves correctly. Developers update the test without gaining any new confidence.&lt;/p&gt;

&lt;p&gt;A single test that verifies a business rule - for example, "an order is rejected when inventory is insufficient" - would provide far more value than multiple tests that mirror the internal structure of the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instead of counting tests, reviewers should ask:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What behavior would break if this test failed?&lt;/li&gt;
&lt;li&gt;Does this test protect a business rule or just code structure?&lt;/li&gt;
&lt;li&gt;Would this test catch a real regression?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small number of meaningful tests is more valuable than broad but shallow coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 6: Code Review Culture Becomes Toxic or Avoidant
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Happens
&lt;/h2&gt;

&lt;p&gt;Two common extremes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;overly aggressive reviews that discourage contributions&lt;/li&gt;
&lt;li&gt;overly polite reviews that avoid real feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both lead to stagnation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Fails
&lt;/h2&gt;

&lt;p&gt;Code review is a social process. If feedback feels unsafe or pointless, developers disengage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Separate People from Code - Explicitly
&lt;/h2&gt;

&lt;p&gt;Healthy teams adopt clear norms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;critique decisions, not individuals&lt;/li&gt;
&lt;li&gt;explain why something matters&lt;/li&gt;
&lt;li&gt;acknowledge good solutions&lt;/li&gt;
&lt;li&gt;use questions to invite discussion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code review should feel like collaboration, not judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 7: Approval Means "Perfect"
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Happens
&lt;/h2&gt;

&lt;p&gt;Reviewers hesitate to approve because they see possible improvements. PRs stall waiting for "one more fix".&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Fails
&lt;/h2&gt;

&lt;p&gt;Perfection is undefined and unattainable. Delayed merging increases risk more than minor imperfections.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Redefine Approval
&lt;/h2&gt;

&lt;p&gt;Approval means:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The risk of merging this change is acceptable given our context."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It does not mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the solution is ideal&lt;/li&gt;
&lt;li&gt;no improvements remain&lt;/li&gt;
&lt;li&gt;no future refactoring will be needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shift alone can dramatically speed up delivery without sacrificing quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Fits Modern Development Workflows
&lt;/h2&gt;

&lt;p&gt;In modern teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automation enforces consistency&lt;/li&gt;
&lt;li&gt;deployments are frequent&lt;/li&gt;
&lt;li&gt;rollbacks are possible&lt;/li&gt;
&lt;li&gt;feedback cycles are short&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code review should complement these realities - not fight them.&lt;/p&gt;

&lt;p&gt;When designed correctly, code review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;spreads knowledge&lt;/li&gt;
&lt;li&gt;reduces hidden risk&lt;/li&gt;
&lt;li&gt;improves decision-making&lt;/li&gt;
&lt;li&gt;strengthens team trust&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When designed poorly, it becomes friction disguised as quality control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Most code review problems are not technical.&lt;br&gt;
They are process and expectation problems.&lt;/p&gt;

&lt;p&gt;Fixing them does not require smarter developers or stricter rules - only clearer boundaries, better questions, and respect for how people actually work.&lt;/p&gt;

&lt;p&gt;When that happens, code review stops being a bottleneck and becomes what it was always meant to be: a shared investment in better software.&lt;/p&gt;

</description>
      <category>codequality</category>
      <category>softwareengineering</category>
      <category>backend</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
