<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>codecraftdiary, Author at CodeCraft Diary</title>
	<atom:link href="https://codecraftdiary.com/author/jwchildroot/feed/" rel="self" type="application/rss+xml" />
	<link>https://codecraftdiary.com/author/jwchildroot/</link>
	<description></description>
	<lastBuildDate>Fri, 10 Apr 2026 09:15:01 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://codecraftdiary.com/wp-content/uploads/2025/10/cropped-IMG_3463-32x32.png</url>
	<title>codecraftdiary, Author at CodeCraft Diary</title>
	<link>https://codecraftdiary.com/author/jwchildroot/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Fat Controller Laravel Refactor: Step-by-Step Clean Architecture Guide</title>
		<link>https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/</link>
					<comments>https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 11 Apr 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3227</guid>

					<description><![CDATA[<p>Refactoring a fat controller in Laravel is one of the most impactful improvements you can make in a growing codebase. As projects evolve, controllers often become overloaded with validation, business logic, and side effects, making them difficult to maintain and test. A controller starts small. Clean. Readable. Then features get added. Deadlines hit. Logic piles [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/">Fat Controller Laravel Refactor: Step-by-Step Clean Architecture Guide</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Refactoring a fat controller in Laravel is one of the most impactful improvements you can make in a growing codebase. As projects evolve, controllers often become overloaded with validation, business logic, and side effects, making them difficult to maintain and test.</p>



<p>A controller starts small. Clean. Readable.</p>



<p>Then features get added.</p>



<p>Deadlines hit.</p>



<p>Logic piles up.</p>



<p>And suddenly, you’re staring at a 500-line controller that handles validation, business logic, database writes, API calls, and maybe even a bit of formatting “just for now.”</p>



<p>This is what we call a <strong>Fat Controller</strong> — and it’s one of the most common maintainability problems in Laravel applications.</p>



<p>In this article, we’ll take a real-world approach and refactor a fat controller into a cleaner, scalable structure using principles inspired by Clean Architecture.</p>



<p>No theory overload. Just practical steps.</p>



<p>Previous Article in this category : <a href="https://codecraftdiary.com/2026/03/21/ai-driven-refactoring/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/03/21/ai-driven-refactoring/</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-991fb41291386a605fae1a2dd509fe05" style="color:#0092eb">The Problem: A Real Fat Controller Example</h1>



<p>Let’s start with something painfully familiar:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class OrderController extends Controller
{
    public function store(Request $request)
    {
        // Validation
        $validated = $request->validate(&#91;
            'user_id' => 'required|exists:users,id',
            'items' => 'required|array',
        &#93;);

        // Business logic
        $total = 0;

        foreach ($validated&#91;'items'&#93; as $item) {
            $product = Product::find($item&#91;'id'&#93;);

            if (!$product) {
                throw new Exception('Product not found');
            }

            if ($product->stock &lt; $item&#91;'quantity'&#93;) {
                throw new Exception('Not enough stock');
            }

            $total += $product->price * $item&#91;'quantity'&#93;;

            $product->stock -= $item&#91;'quantity'&#93;;
            $product->save();
        }

        // Save order
        $order = Order::create([
            'user_id' => $validated&#91;'user_id'&#93;,
            'total' => $total,
        ]);

        // Save items
        foreach ($validated&#91;'items'&#93; as $item) {
            OrderItem::create([
                'order_id' => $order->id,
                'product_id' => $item&#91;'id'&#93;,
                'quantity' => $item&#91;'quantity'&#93;,
            ]);
        }

        // External API call
        Http::post('https://example.com/webhook', &#91;
            'order_id' => $order->id,
        &#93;);

        return response()->json($order);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderController</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Controller</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">store</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Request</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Validation</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">validate</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;required|exists:users,id&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;required|array&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Business logic</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$total</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">0</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93; as </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (!</span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Exception</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Product not found&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stock</span><span style="color: #D4D4D4"> &lt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Exception</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Not enough stock&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$total</span><span style="color: #D4D4D4"> += </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">price</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stock</span><span style="color: #D4D4D4"> -= </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">save</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Save order</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">([</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;total&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$total</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        ]);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Save items</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93; as </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderItem</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">([</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;order_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            ]);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// External API call</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;https://example.com/webhook&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;order_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">What’s wrong here?</h3>



<ul class="wp-block-list">
<li>Controller handles <strong>too many responsibilities</strong></li>



<li>Business logic is <strong>not reusable</strong></li>



<li>Hard to <strong>test</strong></li>



<li>Tight coupling to Eloquent and external APIs</li>



<li>Changes are risky</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-e47101587334084f3316d73a1a00182e" style="color:#0092eb">Goal: What “Clean” Looks Like</h1>



<p>We want to move toward:</p>



<ul class="wp-block-list">
<li>Thin controllers</li>



<li>Isolated business logic</li>



<li>Testable services</li>



<li>Clear boundaries between layers</li>
</ul>



<p>We’re not going full academic Clean Architecture. We’re applying <strong>just enough structure to stay sane</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-b05d68ecdcfd94e86d88750074971eec" style="color:#0092eb">Step 1: Extract Business Logic into a Service</h1>



<p>First, move the core logic out of the controller.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class CreateOrderService
{
    public function handle(array $data): Order
    {
        $total = 0;

        foreach ($data&#91;'items'&#93; as $item) {
            $product = Product::find($item&#91;'id'&#93;);

            if (!$product) {
                throw new Exception('Product not found');
            }

            if ($product->stock &lt; $item&#91;'quantity'&#93;) {
                throw new Exception('Not enough stock');
            }

            $total += $product->price * $item&#91;'quantity'&#93;;

            $product->stock -= $item&#91;'quantity'&#93;;
            $product->save();
        }

        $order = Order::create([
            'user_id' => $data&#91;'user_id'&#93;,
            'total' => $total,
        ]);

        foreach ($data&#91;'items'&#93; as $item) {
            OrderItem::create([
                'order_id' => $order->id,
                'product_id' => $item&#91;'id'&#93;,
                'quantity' => $item&#91;'quantity'&#93;,
            ]);
        }

        Http::post('https://example.com/webhook', &#91;
            'order_id' => $order->id,
        &#93;);

        return $order;
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CreateOrderService</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">Order</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$total</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">0</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93; as </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (!</span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Exception</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Product not found&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stock</span><span style="color: #D4D4D4"> &lt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Exception</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Not enough stock&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$total</span><span style="color: #D4D4D4"> += </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">price</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stock</span><span style="color: #D4D4D4"> -= </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">save</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">([</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;total&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$total</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        ]);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93; as </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderItem</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">([</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;order_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            ]);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;https://example.com/webhook&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;order_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Controller becomes:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class OrderController extends Controller
{
    public function store(Request $request, CreateOrderService $service)
    {
        $validated = $request->validate(&#91;
            'user_id' => 'required|exists:users,id',
            'items' => 'required|array',
        &#93;);

        $order = $service->handle($validated);

        return response()->json($order);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderController</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Controller</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">store</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Request</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">CreateOrderService</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">validate</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;required|exists:users,id&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;required|array&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">Improvement:</h3>



<ul class="wp-block-list">
<li>Controller is now thin</li>



<li>Logic is reusable</li>



<li>Easier to test</li>
</ul>



<p>But we’re not done yet.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-8d12f904a0f9b26ed6db0c892e409c2e" style="color:#0092eb">Step 2: Introduce a Data Transfer Object (DTO)</h1>



<p>Passing raw arrays is fragile.</p>



<p>Let’s fix that.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class CreateOrderData
{
    public function __construct(
        public int $userId,
        public array $items
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            $data&#91;'user_id'&#93;,
            $data&#91;'items'&#93;
        );
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CreateOrderData</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$userId</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$items</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">fromArray</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">self</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93;</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Update controller:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$data = CreateOrderData::fromArray($validated);
$order = $service->handle($data);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">CreateOrderData</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fromArray</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Update service:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function handle(CreateOrderData $data): Order
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">CreateOrderData</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">Order</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">Improvement:</h3>



<ul class="wp-block-list">
<li>Stronger typing</li>



<li>Safer refactoring</li>



<li>Clear contract</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-2a5dd61e85b550e82acf11ca5c464d2f" style="color:#0092eb">Step 3: Decouple External Dependencies</h1>



<p>Right now, your service is tightly coupled to:</p>



<ul class="wp-block-list">
<li>Eloquent models</li>



<li>HTTP client</li>
</ul>



<p>Let’s extract the webhook logic.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class OrderWebhookService
{
    public function send(Order $order): void
    {
        Http::post('https://example.com/webhook', &#91;
            'order_id' => $order->id,
        &#93;);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderWebhookService</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">send</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;https://example.com/webhook&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;order_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Inject it:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class CreateOrderService
{
    public function __construct(
        private OrderWebhookService $webhook
    ) {}

    public function handle(CreateOrderData $data): Order
    {
        // logic...

        $this->webhook->send($order);

        return $order;
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CreateOrderService</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderWebhookService</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$webhook</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">CreateOrderData</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">Order</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// logic...</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">webhook</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">send</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">Improvement:</h3>



<ul class="wp-block-list">
<li>External side effects are isolated</li>



<li>Easier to mock in tests</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-e473ee78c02ca54fb8bbff3263948cbd" style="color:#0092eb">Step 4: Make It Testable</h1>



<p>Now you can test the service independently:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function test_it_creates_order()
{
    $service = app(CreateOrderService::class);

    $data = new CreateOrderData(
        userId: 1,
        items: [
            &#91;'id' => 1, 'quantity' => 2&#93;,
        ]
    );

    $order = $service->handle($data);

    $this->assertNotNull($order->id);
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_it_creates_order</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4"> = </span><span style="color: #DCDCAA">app</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">CreateOrderService</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CreateOrderData</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        userId: </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        items: [</span></span>
<span class="line"><span style="color: #D4D4D4">            &#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">2</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">        ]</span></span>
<span class="line"><span style="color: #D4D4D4">    );</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertNotNull</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Before refactoring, this would require:</p>



<ul class="wp-block-list">
<li>HTTP mocking</li>



<li>Controller testing</li>



<li>Complex setup</li>
</ul>



<p>Now it’s isolated.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-f071ab6b974484e9595c4a2b5f5a6fd2" style="color:#0092eb">Step 5: Optional — Introduce Repositories (Only If Needed)</h1>



<p>Don’t overengineer this.</p>



<p>But if your app grows, you might extract:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class ProductRepository
{
    public function find(int $id): ?Product
    {
        return Product::find($id);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ProductRepository</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">): ?</span><span style="color: #4EC9B0">Product</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Then inject it into the service.</p>



<h3 class="wp-block-heading">When to do this:</h3>



<ul class="wp-block-list">
<li>Multiple data sources</li>



<li>Complex queries</li>



<li>Domain logic reuse</li>
</ul>



<h3 class="wp-block-heading">When NOT to:</h3>



<ul class="wp-block-list">
<li>Simple CRUD apps</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-9c42f58011d4e629a65d0596dbcd328d" style="color:#0092eb">Before vs After</h1>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Aspect</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Controller size</td><td>Huge</td><td>Minimal</td></tr><tr><td>Testability</td><td>Hard</td><td>Easy</td></tr><tr><td>Reusability</td><td>None</td><td>High</td></tr><tr><td>Coupling</td><td>High</td><td>Reduced</td></tr><tr><td>Maintainability</td><td>Painful</td><td>Scalable</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-7642e60fdb05c923f655fe5d8071a27d" style="color:#0092eb">Common Mistakes to Avoid</h1>



<h3 class="wp-block-heading">1. Moving everything blindly into services</h3>



<p>You’ll just create <strong>fat services instead of fat controllers</strong>.</p>



<p>Keep services focused.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. Overengineering with too many layers</h3>



<p>You don’t need:</p>



<ul class="wp-block-list">
<li>10 interfaces</li>



<li>5 abstractions</li>



<li>enterprise architecture™</li>
</ul>



<p>Start simple. Evolve when needed.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. Ignoring boundaries</h3>



<p>Controllers = HTTP<br>Services = business logic<br>Models = persistence</p>



<p>Mixing these again = back to chaos.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-8debc7dd1b41b90e3f47eecd871a3323" style="color:#0092eb">Key Takeaways</h1>



<ul class="wp-block-list">
<li>Fat controllers are a <strong>symptom</strong>, not the root problem</li>



<li>The real issue is <strong>mixed responsibilities</strong></li>



<li>Start with <strong>service extraction</strong></li>



<li>Add DTOs for safety</li>



<li>Isolate side effects (APIs, events)</li>



<li>Only introduce more abstraction when justified</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-129cdfa1bc4360e7ce866fb1cfc8eebc" style="color:#0092eb">Final Thought</h1>



<p>Clean Architecture in Laravel doesn’t mean rewriting your app into a textbook diagram.</p>



<p>It means one thing:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Making your code easier to change without fear.</p>
</blockquote>



<p>And the fastest way to get there?</p>



<p>Start killing your fat controllers — one method at a time.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>If this is something you’re dealing with right now, your next step is simple:</p>



<p>Pick your worst controller and extract just one action into a service.</p>



<p>That’s how clean architecture actually starts.</p>
<p>The post <a href="https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/">Fat Controller Laravel Refactor: Step-by-Step Clean Architecture Guide</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Trunk-Based Development: Why Most Teams Think They Use It (But Don’t)</title>
		<link>https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/</link>
					<comments>https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/#comments</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 04 Apr 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Development Workflow & Best Practices]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[workflow]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3222</guid>

					<description><![CDATA[<p>Trunk-Based Development sounds simple. No long-lived branches.Frequent merges.Small, incremental changes. Most teams will tell you: “Yeah, we basically do trunk-based development.” In practice, they don’t. What they usually have is a hybrid that keeps the downsides of feature branches — while pretending to get the benefits of trunk-based development. I’ve seen this pattern in multiple [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/">Trunk-Based Development: Why Most Teams Think They Use It (But Don’t)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Trunk-Based Development sounds simple.</p>



<p>No long-lived branches.<br>Frequent merges.<br>Small, incremental changes.</p>



<p>Most teams will tell you:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“Yeah, we basically do trunk-based development.”</p>
</blockquote>



<p>In practice, they don’t.</p>



<p>What they usually have is a hybrid that keeps the downsides of feature branches — while pretending to get the benefits of trunk-based development.</p>



<p>I’ve seen this pattern in multiple backend teams. On paper, everything looks modern. In reality, delivery is still slow, pull requests are large, and integration is painful.</p>



<p>So let’s break down what’s actually going on.</p>



<p>Last article in this category: <a href="https://codecraftdiary.com/2026/03/15/small-pull-requests/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/03/15/small-pull-requests/</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-e5703807871822d47e4cf731396df980" style="color:#0092eb">The Illusion of Trunk-Based Development</h1>



<p>Ask a team how they work, and you’ll often hear something like:</p>



<ul class="wp-block-list">
<li>“We merge to main frequently”</li>



<li>“We don’t keep branches for too long”</li>



<li>“We try to keep PRs small”</li>
</ul>



<p>Sounds good.</p>



<p>But then you look closer:</p>



<ul class="wp-block-list">
<li>PRs are open for 3–5 days</li>



<li>branches still contain multiple features</li>



<li>merges are delayed because reviews are slow</li>



<li>developers are afraid to merge unfinished work</li>
</ul>



<p>This is not trunk-based development.</p>



<p>This is just <strong>shorter feature branches</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-10620f000e32dcee9567d869095bfec6" style="color:#0092eb">What Real Trunk-Based Development Actually Requires</h1>



<p>Trunk-based development is not about branches.</p>



<p>It’s about <strong>integration frequency and safety</strong>.</p>



<p>At its core, it requires:</p>



<ul class="wp-block-list">
<li>merging to main at least daily (ideally multiple times per day)</li>



<li>keeping changes small enough to review quickly</li>



<li>having strong safety mechanisms in place</li>
</ul>



<p>Without these, trunk-based development collapses.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-d245389f0bd2c45912ec8b8b2b755187" style="color:#0092eb">Where Most Teams Break</h1>



<h2 class="wp-block-heading">1. Pull Requests Are Still Too Big</h2>



<p>This is the biggest issue.</p>



<p>A developer starts a “small” feature:</p>



<ul class="wp-block-list">
<li>adds endpoint</li>



<li>updates service</li>



<li>modifies database</li>



<li>adds validation</li>



<li>fixes something unrelated</li>
</ul>



<p>Now the PR has:</p>



<ul class="wp-block-list">
<li>15 files changed</li>



<li>600+ lines</li>



<li>multiple concerns</li>
</ul>



<p>Review slows down. Feedback increases. Merge is delayed.</p>



<p>At that point, it doesn’t matter what you call your workflow —<br>you’re not doing trunk-based development.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">2. Code Reviews Block Integration</h2>



<p>In theory:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“We merge frequently”</p>
</blockquote>



<p>In reality:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“We merge when the PR gets approved”</p>
</blockquote>



<p>That delay is critical.</p>



<p>If reviews take:</p>



<ul class="wp-block-list">
<li>1 day → integration is delayed</li>



<li>2–3 days → conflicts increase</li>



<li>5 days → context is lost</li>
</ul>



<p>Now developers start stacking changes on top of each other.</p>



<p>And suddenly:</p>



<ul class="wp-block-list">
<li>branches live longer</li>



<li>PRs get bigger</li>



<li>merges become risky</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">3. Teams Are Afraid to Merge Incomplete Work</h2>



<p>This is subtle but important.</p>



<p>Developers often think:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“I can’t merge this yet — it’s not finished”</p>
</blockquote>



<p>So they keep working on the branch.</p>



<p>The problem:</p>



<ul class="wp-block-list">
<li>the longer the branch lives</li>



<li>the more it diverges</li>



<li>the harder it is to merge</li>
</ul>



<p>Trunk-based development requires a different mindset:</p>



<p>You merge incomplete work safely.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-6649ee8814ec19eed42444fcc334b851" style="color:#0092eb">The Missing Piece: Feature Flags</h1>



<p>Most teams skip this.</p>



<p>And without it, trunk-based development doesn’t work.</p>



<h3 class="wp-block-heading">Example</h3>



<p>You’re building a new payment flow.</p>



<p>Without feature flags:</p>



<ul class="wp-block-list">
<li>you need to finish everything before merging</li>



<li>you keep a long-lived branch</li>



<li>integration is delayed</li>
</ul>



<p>With feature flags:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>if (Feature::enabled('new_payment_flow')) {
    $this->newFlow();
} else {
    $this->oldFlow();
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #4EC9B0">Feature</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">enabled</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;new_payment_flow&#39;</span><span style="color: #D4D4D4">)) {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">newFlow</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">} </span><span style="color: #C586C0">else</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">oldFlow</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Now you can:</p>



<ul class="wp-block-list">
<li>merge partial work</li>



<li>deploy continuously</li>



<li>control exposure</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-933388475fdebcc1aa2ed5cd727e37dd" style="color:#0092eb">Real Example From Practice</h1>



<p>I worked with a team that claimed they were doing trunk-based development.</p>



<p>Metrics looked like this:</p>



<ul class="wp-block-list">
<li>average PR size: ~500 lines</li>



<li>review time: 2–3 days</li>



<li>merges per developer: ~2 per week</li>
</ul>



<p>After digging in, the issue was clear:</p>



<ul class="wp-block-list">
<li>developers grouped work “to be efficient”</li>



<li>reviews were asynchronous and slow</li>



<li>no feature flags</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">What changed</h3>



<p>We introduced 3 rules:</p>



<ol class="wp-block-list">
<li>PR must be mergeable within the same day</li>



<li>no PR over ~300 lines (soft limit)</li>



<li>feature flags for incomplete features</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Result after ~3 weeks:</h3>



<ul class="wp-block-list">
<li>PR size dropped by ~40%</li>



<li>review time dropped to hours</li>



<li>merges increased to multiple per day</li>



<li>production issues decreased</li>
</ul>



<p>Not because developers got better.</p>



<p>Because the system changed.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-109682028e39434a34e60100cc836c25" style="color:#0092eb">The Hidden Constraint: CI/CD Speed</h1>



<p>Here’s something teams often ignore:</p>



<p>You cannot do trunk-based development with slow pipelines.</p>



<p>If your CI takes:</p>



<ul class="wp-block-list">
<li>20 minutes → friction</li>



<li>40 minutes → developers wait</li>



<li>60+ minutes → people stop merging frequently</li>
</ul>



<p>So what happens?</p>



<ul class="wp-block-list">
<li>developers batch changes</li>



<li>PRs get bigger</li>



<li>integration slows down</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-6fb1273a52543b70a0bd6271ffea68fd" style="color:#0092eb">Rule of thumb (2026 reality):</h3>



<ul class="wp-block-list">
<li>CI under 10 minutes → good</li>



<li>under 5 minutes → ideal</li>
</ul>



<p>Anything above that:<br>→ you’re actively fighting your workflow</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-ad88f6df73721dbf0a4fc8fa61593aa1" style="color:#0092eb">Why This Matters More in 2026</h1>



<p>With AI-assisted coding, developers can generate code faster than ever.</p>



<p>That creates a new problem:</p>



<p><strong>volume of changes increases</strong></p>



<p><strong>but review capacity doesn’t</strong></p>



<p>If you don’t enforce:</p>



<ul class="wp-block-list">
<li>small changes</li>



<li>fast integration</li>



<li>clear boundaries</li>
</ul>



<p>your workflow collapses under its own weight.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-414578f574201477fe295a4ea10c5753" style="color:#0092eb">How to Tell If You’re Actually Doing It</h1>



<p>Be honest and check:</p>



<ul class="wp-block-list">
<li>Do you merge to main multiple times per day?</li>



<li>Are most PRs reviewed within hours, not days?</li>



<li>Can you safely merge incomplete work?</li>



<li>Are branches short-lived (hours, not days)?</li>
</ul>



<p>If not:<br>→ you’re not doing trunk-based development</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-a7885f2e9c5b39e916fd3b30c633caf1" style="color:#0092eb">Practical Rules You Can Apply Tomorrow</h1>



<p>If you want to move closer to real trunk-based development, start here:</p>



<h3 class="wp-block-heading">1. Limit PR size</h3>



<p>Not as a guideline — as a rule.</p>



<h3 class="wp-block-heading">2. Optimize for review speed</h3>



<ul class="wp-block-list">
<li>fewer changes</li>



<li>clearer intent</li>



<li>less context switching</li>
</ul>



<h3 class="wp-block-heading">3. Introduce feature flags</h3>



<p>Without them, you’re stuck.</p>



<h3 class="wp-block-heading">4. Fix your CI/CD bottlenecks</h3>



<p>Speed is not a luxury — it’s a requirement.</p>



<h3 class="wp-block-heading">5. Stop batching work</h3>



<p>Batching feels efficient.<br>It’s not.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-adb942a2a2401ff63aa1a0d3b7e3ab73" style="color:#0092eb">Closing Thought</h1>



<p>Trunk-based development is not a branching strategy.</p>



<p>It’s a discipline.</p>



<p>Most teams don’t fail because they don’t understand it.<br>They fail because they don’t change the constraints that make it possible.</p>



<p>If your:</p>



<ul class="wp-block-list">
<li>PRs are big</li>



<li>reviews are slow</li>



<li>CI is slow</li>



<li>merges are risky</li>
</ul>



<p>then it doesn’t matter what you call your workflow.</p>



<p>You’re still doing feature-branch development — just with better branding.</p>



<p>And that’s exactly why your delivery still feels slower than it should.</p>



<p></p>
<p>The post <a href="https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/">Trunk-Based Development: Why Most Teams Think They Use It (But Don’t)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Laravel RefreshDatabase vs DatabaseTransactions: When to Use Each</title>
		<link>https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/</link>
					<comments>https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 28 Mar 2026 09:26:07 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3217</guid>

					<description><![CDATA[<p>Laravel RefreshDatabase vs DatabaseTransactions is one of the most common sources of confusion when writing tests in Laravel. Choosing the wrong approach can lead to flaky tests, hidden bugs, and unreliable results. When writing tests in Laravel, database state can quickly become a source of confusion. One test passes, another fails. Data seems to “leak” [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/">Laravel RefreshDatabase vs DatabaseTransactions: When to Use Each</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Laravel RefreshDatabase vs DatabaseTransactions is one of the most common sources of confusion when writing tests in Laravel.</p>



<p>Choosing the wrong approach can lead to flaky tests, hidden bugs, and unreliable results.</p>



<p>When writing tests in Laravel, database state can quickly become a source of confusion.</p>



<p>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.</p>



<p>If you’ve experienced this, you’re not alone.</p>



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



<ul class="wp-block-list">
<li><code>RefreshDatabase</code></li>



<li><code>DatabaseTransactions</code></li>
</ul>



<p>At first glance, they seem similar. In reality, they behave very differently — and choosing the wrong one can lead to subtle bugs, flaky tests, or even false confidence in your test suite.</p>



<p>In this guide, you’ll learn exactly how both approaches work, their trade-offs, and when to use each in real-world scenarios.</p>



<p>Last article in this category: <a href="https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/</a></p>



<p>PHP Unit docs: <a href="https://phpunit.de/documentation.html" target="_blank" rel="noreferrer noopener">https://phpunit.de/documentation.html</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-c6e218a0c97a35374efcf4f8efaa547b" style="color:#0092eb">Understanding the Core Problem</h2>



<p>Before diving into Laravel specifics, let’s clarify the core issue:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Tests must be isolated.</p>
</blockquote>



<p>Each test should run independently, without being affected by previous tests.</p>



<p>If your database is not reset properly between tests:</p>



<ul class="wp-block-list">
<li>data can persist unexpectedly</li>



<li>test results become unreliable</li>



<li>debugging becomes painful</li>
</ul>



<p>Laravel solves this problem using two strategies:</p>



<ol class="wp-block-list">
<li>Reset the database completely</li>



<li>Wrap each test in a transaction and roll it back</li>
</ol>



<p><strong>Article about practice feature testing in PHP:</strong> <a href="https://codecraftdiary.com/2025/10/30/feature-testing-in-php-ensuring-the-whole-system-works-together/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2025/10/30/feature-testing-in-php-ensuring-the-whole-system-works-together/</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-ade6712a79372d81dabc43ea731180b0" style="color:#0092eb">Option 1: RefreshDatabase</h2>



<p>The <code>RefreshDatabase</code> trait is the most commonly used solution.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>use Illuminate\Foundation\Testing\RefreshDatabase;

class UserTest extends TestCase
{
    use RefreshDatabase;
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Foundation\Testing\</span><span style="color: #4EC9B0">RefreshDatabase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">UserTest</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TestCase</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">RefreshDatabase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-05ef3242520ca2feef45f6564ae02e64" style="color:#0092eb">How it works</h3>



<ul class="wp-block-list">
<li>Runs migrations before tests</li>



<li>Ensures a clean database state</li>



<li>Uses transactions internally when possible</li>
</ul>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-5f38fe276422fcbf8648d34166b9618e" style="color:#0092eb">What this means in practice</h3>



<p>Every test starts with:</p>



<ul class="wp-block-list">
<li>a fresh schema</li>



<li>no leftover data</li>



<li>consistent environment</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-023549e5f6f504bf423c5d1a979daebc" style="color:#0092eb">Advantages</h3>



<p><strong>1. High reliability</strong></p>



<p>Each test runs in a completely clean environment.</p>



<p><strong>2. Works with everything</strong></p>



<ul class="wp-block-list">
<li>queues</li>



<li>jobs</li>



<li>events</li>



<li>external processes</li>
</ul>



<p><strong>3. Predictable behavior</strong></p>



<p>No surprises caused by hidden state.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-2fb46e6663a94f5381e433bbbbde178e" style="color:#0092eb">Disadvantages</h3>



<p><strong>1. Slower</strong></p>



<p>Running migrations repeatedly can add overhead.</p>



<p><strong>2. Heavier setup</strong></p>



<p>Especially noticeable in large applications.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-79c469f983c5a90a21512800fcf8011d" style="color:#0092eb">Option 2: DatabaseTransactions</h2>



<p>The <code>DatabaseTransactions</code> trait takes a different approach.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>use Illuminate\Foundation\Testing\DatabaseTransactions;

class UserTest extends TestCase
{
    use DatabaseTransactions;
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Foundation\Testing\</span><span style="color: #4EC9B0">DatabaseTransactions</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">UserTest</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TestCase</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">DatabaseTransactions</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-413a28475af66eafbd8a06b635a9d87f" style="color:#0092eb">How it works</h3>



<ul class="wp-block-list">
<li>Starts a database transaction before each test</li>



<li>Rolls it back after the test finishes</li>
</ul>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-2c950b4ef6a692be73f837b5323c6df1" style="color:#0092eb">What this means</h3>



<p>Instead of resetting the database:</p>



<ul class="wp-block-list">
<li>changes are never committed</li>



<li>everything is undone automatically</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-dd7684807916e331d119b65a0cbd781b" style="color:#0092eb">Advantages</h3>



<p><strong>1. Fast</strong></p>



<p>Transactions are significantly quicker than migrations.</p>



<p><strong>2. Lightweight</strong></p>



<p>No need to rebuild the database schema.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-55126bd221bc861e06d1affdd86f83b2" style="color:#0092eb">Disadvantages</h3>



<p><strong>1. Limited scope</strong></p>



<p>Does NOT work well with:</p>



<ul class="wp-block-list">
<li>queued jobs</li>



<li>async processes</li>



<li>separate DB connections</li>
</ul>



<p><strong>2. Hidden pitfalls</strong></p>



<p>If something runs outside the transaction:</p>



<ul class="wp-block-list">
<li>it won’t be rolled back</li>



<li>tests may behave inconsistently</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-1215ad144b1b6eb53b80ca2765316a7e" style="color:#0092eb">Key Differences</h2>



<p>Here’s a clear comparison:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Feature</th><th>RefreshDatabase</th><th>DatabaseTransactions</th></tr></thead><tbody><tr><td>Speed</td><td>Slower</td><td>Faster</td></tr><tr><td>Isolation</td><td>High</td><td>Medium</td></tr><tr><td>Works with queues</td><td>Yes</td><td>No</td></tr><tr><td>Works across connections</td><td>Yes</td><td>No</td></tr><tr><td>Setup complexity</td><td>Medium</td><td>Low</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-a221f1330efe31d2991ce26b6acb32bd" style="color:#0092eb">When to Use RefreshDatabase</h2>



<p>Use <code>RefreshDatabase</code> when:</p>



<h3 class="wp-block-heading">1. You are testing queues or jobs</h3>



<p>Jobs often run outside the main transaction.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>dispatch(new ProcessOrderJob($order));
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ProcessOrderJob</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">));</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>If you use <code>DatabaseTransactions</code>, the job may not see the data at all.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. You rely on multiple database connections</h3>



<p>Transactions don’t span across connections reliably.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. You want maximum reliability</h3>



<p>If your priority is correctness over speed, this is the safer option.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-4e55e01a3bba0b1876da3f9a41c8d234" style="color:#0092eb">When to Use DatabaseTransactions</h2>



<p>Use <code>DatabaseTransactions</code> when:</p>



<h3 class="wp-block-heading">1. You are testing simple database interactions</h3>



<p>Example:</p>



<ul class="wp-block-list">
<li>repository logic</li>



<li>basic CRUD operations</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. Performance matters</h3>



<p>In large test suites, this can significantly reduce execution time.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. Everything runs in a single request lifecycle</h3>



<p>No queues, no async behavior, no external processes.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-4376598e5350657cfe30bbd20fea34d7" style="color:#0092eb">Common Mistakes (and How to Avoid Them)</h2>



<h3 class="wp-block-heading">Mixing both traits</h3>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>use RefreshDatabase, DatabaseTransactions;
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">RefreshDatabase</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">DatabaseTransactions</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>This creates unpredictable behavior.</p>



<p><strong>Fix:</strong> Choose one strategy per test class.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Using transactions with queues</h3>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>Queue::fake();
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Even with fakes, underlying behavior may break if data isn’t committed.</p>



<p><strong>Fix:</strong> Use <code>RefreshDatabase</code> for queue-related tests.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Assuming transactions isolate everything</h3>



<p>They don’t.</p>



<p>Anything outside the transaction:</p>



<ul class="wp-block-list">
<li>won’t be rolled back</li>



<li>can pollute your test environment</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Ignoring test failures caused by state</h3>



<p>Flaky tests often indicate:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>improper database isolation</p>
</blockquote>



<p>Don’t ignore them — fix the root cause.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-c87de7047cbba7b381ba62bc6125ccc9" style="color:#0092eb">Real-World Example</h2>



<p>Let’s say you’re testing order processing.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function test_order_is_processed()
{
    $order = Order::factory()->create();

    dispatch(new ProcessOrderJob($order));

    $this->assertDatabaseHas('orders', &#91;
        'id' => $order->id,
        'status' => 'processed',
    &#93;);
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_order_is_processed</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ProcessOrderJob</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">));</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;processed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">With DatabaseTransactions</h3>



<ul class="wp-block-list">
<li>job may not see the order</li>



<li>test fails unexpectedly</li>
</ul>



<h3 class="wp-block-heading">With RefreshDatabase</h3>



<ul class="wp-block-list">
<li>data is committed</li>



<li>job works correctly</li>



<li>test passes</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-6255e82282614954f02fb30b92e2a772" style="color:#0092eb">Recommended Strategy</h2>



<p>For most Laravel applications, the safest default is:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>use RefreshDatabase;
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">RefreshDatabase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Then optimize later if needed.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-157cc1e9415d94989fe4813f564732d5" style="color:#0092eb">Hybrid approach (advanced)</h3>



<p>You can:</p>



<ul class="wp-block-list">
<li>use <code>RefreshDatabase</code> for feature tests</li>



<li>use <code>DatabaseTransactions</code> for isolated unit-like tests</li>
</ul>



<p>This gives you:</p>



<ul class="wp-block-list">
<li>reliability where needed</li>



<li>speed where possible</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-c114ec3af8a7e3501b1ba99c39788434" style="color:#0092eb">Final Thoughts</h2>



<p>Database testing in Laravel isn’t just about writing assertions — it’s about controlling state.</p>



<p>Choosing between <code>RefreshDatabase</code> and <code>DatabaseTransactions</code> directly affects:</p>



<ul class="wp-block-list">
<li>test reliability</li>



<li>debugging time</li>



<li>overall confidence in your application</li>
</ul>



<p>If you’re unsure which one to use, start with:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>RefreshDatabase</code></p>
</blockquote>



<p>It may be slightly slower, but it will save you hours of debugging and prevent subtle, hard-to-track issues.</p>



<p>Once your test suite grows, you can selectively introduce <code>DatabaseTransactions</code> where performance matters and constraints are well understood.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-f4f5589d16b432ea58c9464c316b3660" style="color:#0092eb">TL;DR</h2>



<ul class="wp-block-list">
<li>Use <strong>RefreshDatabase</strong> for reliability and real-world scenarios</li>



<li>Use <strong>DatabaseTransactions</strong> for speed in simple cases</li>



<li>Avoid mixing both</li>



<li>Be mindful of queues and async behavior</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>A well-structured test suite isn’t just about coverage — it’s about trust.</p>



<p>And that trust starts with a clean, predictable database.</p>



<p></p>
<p>The post <a href="https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/">Laravel RefreshDatabase vs DatabaseTransactions: When to Use Each</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AI-Driven Refactoring in PHP: When to Trust Copilot (and When to Take the Wheel)</title>
		<link>https://codecraftdiary.com/2026/03/21/ai-driven-refactoring/</link>
					<comments>https://codecraftdiary.com/2026/03/21/ai-driven-refactoring/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 21 Mar 2026 10:26:59 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<category><![CDATA[software-design]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3210</guid>

					<description><![CDATA[<p>In 2026, the biggest performance gains in PHP don’t come from micro-optimizations—they come from I/O strategy. 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. But this is also where AI tools like Copilot become dangerous. [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/03/21/ai-driven-refactoring/">AI-Driven Refactoring in PHP: When to Trust Copilot (and When to Take the Wheel)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>In 2026, the biggest performance gains in PHP don’t come from micro-optimizations—they come from <strong>I/O strategy</strong>.</p>



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



<p>But this is also where AI tools like Copilot become dangerous.</p>



<p>They can accelerate the refactor—or quietly corrupt your architecture.</p>



<p>Previous article in this category: <a href="https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/" type="link" id="https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-21e2871efe7430c871401b87d52a6a73" style="color:#0092eb">The “Prompt–Refactor–Verify” Cycle</h1>



<p>When working with AI, the difference between success and subtle bugs is <strong>prompt precision</strong>.</p>



<p>Bad prompt:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“Make this async.”</p>
</blockquote>



<p>Good prompt:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“Identify I/O-bound operations. Refactor using Fibers with an event loop (Revolt/Amp). Avoid shared mutable state. Ensure deterministic completion.”</p>
</blockquote>



<p>If you paste a 500-line method without context, AI will:</p>



<ul class="wp-block-list">
<li>miss hidden dependencies</li>



<li>ignore state coupling</li>



<li>introduce non-obvious bugs</li>
</ul>



<p>AI is a transformer, not a system thinker.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-7a7b6c81ce004406d469c637f4ccfad0" style="color:#0092eb">Real-World Example: Legacy Image Processor</h1>



<h3 class="wp-block-heading">Before (typical legacy flow)</h3>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>foreach ($urls as $url) {
    $image = $this->download($url);   // blocking
    $resized = $this->resize($image); // CPU
    $this->upload($resized);          // blocking
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$urls</span><span style="color: #D4D4D4"> as </span><span style="color: #9CDCFE">$url</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$image</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">download</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$url</span><span style="color: #D4D4D4">);   </span><span style="color: #6A9955">// blocking</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$resized</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">resize</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$image</span><span style="color: #D4D4D4">); </span><span style="color: #6A9955">// CPU</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">upload</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$resized</span><span style="color: #D4D4D4">);          </span><span style="color: #6A9955">// blocking</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Simple. Predictable. Slow.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">After (controlled concurrency with Amp)</h3>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>use Amp\Future;
use function Amp\async;

$tasks = [];

foreach ($urls as $url) {
    $tasks[] = async(function () use ($url) {
        $image = $this->download($url);   // non-blocking if using async client
        $resized = $this->resize($image);
        return $this->upload($resized);
    });
}

$results = Future\awaitAll($tasks);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Amp\</span><span style="color: #4EC9B0">Future</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> Amp\</span><span style="color: #4EC9B0">async</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #9CDCFE">$tasks</span><span style="color: #D4D4D4"> = [];</span></span>
<span class="line"></span>
<span class="line"><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$urls</span><span style="color: #D4D4D4"> as </span><span style="color: #9CDCFE">$url</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$tasks</span><span style="color: #D4D4D4">[] = </span><span style="color: #DCDCAA">async</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> () </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$url</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$image</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">download</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$url</span><span style="color: #D4D4D4">);   </span><span style="color: #6A9955">// non-blocking if using async client</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$resized</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">resize</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$image</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">upload</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$resized</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    });</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #9CDCFE">$results</span><span style="color: #D4D4D4"> = Future\</span><span style="color: #DCDCAA">awaitAll</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$tasks</span><span style="color: #D4D4D4">);</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">What changed?</h3>



<ul class="wp-block-list">
<li>I/O is now concurrent</li>



<li>Execution is <strong>interleaved</strong>, not parallel threads</li>



<li>The bottleneck shifts from waiting → throughput</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-c7e2a95049f2ff8878c8202be32e326c" style="color:#0092eb">The AI Trap: “It Works” ≠ “It’s Safe”</h1>



<p>AI often produces code that <em>looks</em> correct and even passes tests.</p>



<p>But there are hidden risks.</p>



<h3 class="wp-block-heading">1. Shared State Issues</h3>



<p>This is dangerous:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$this->processedUrls[] = $result;</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">processedUrls</span><span style="color: #D4D4D4">[] = </span><span style="color: #9CDCFE">$result</span><span style="color: #D4D4D4">;</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Not because PHP is multithreaded—but because:</p>



<ul class="wp-block-list">
<li>execution is interleaved</li>



<li>order is no longer deterministic</li>



<li>side effects accumulate unpredictably</li>
</ul>



<p> Fix:</p>



<ul class="wp-block-list">
<li>return values → aggregate later</li>



<li>avoid mutation inside async tasks</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. The “AI Echo Chamber”</h3>



<p>One of the most dangerous patterns:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>AI writes a bug → AI writes a test → test passes</p>
</blockquote>



<p>Example:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// Bug: returns null on failure
return $result ?? null;</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #6A9955">// Bug: returns null on failure</span></span>
<span class="line"><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$result</span><span style="color: #D4D4D4"> ?? </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">;</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>AI-generated test:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>expect($service->process())->toBeNull();</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #DCDCAA">expect</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">process</span><span style="color: #D4D4D4">())-&gt;</span><span style="color: #DCDCAA">toBeNull</span><span style="color: #D4D4D4">();</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Everything is green. Everything is wrong.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. “Happy Path” Bias</h3>



<p>AI prefers:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>try {
    ...
} catch (\Throwable $e) {
    return null;
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #C586C0">try</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">    ...</span></span>
<span class="line"><span style="color: #D4D4D4">} </span><span style="color: #C586C0">catch</span><span style="color: #D4D4D4"> (\</span><span style="color: #4EC9B0">Throwable</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$e</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>In production systems, this is data loss.</p>



<p>Senior-level expectation:</p>



<ul class="wp-block-list">
<li>explicit exceptions</li>



<li>domain-level error handling</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-f377e3775738154f9e71d533f143029f" style="color:#0092eb">Preventing Architecture Drift</h1>



<p>The biggest long-term risk isn’t bugs.</p>



<p>It’s <strong>inconsistency</strong>.</p>



<p>You refactor:</p>



<ul class="wp-block-list">
<li>Class A on Monday</li>



<li>Class B on Tuesday</li>
</ul>



<p>By Friday:</p>



<ul class="wp-block-list">
<li>patterns diverge</li>



<li>abstractions don’t align</li>
</ul>



<p>Why?</p>



<p>Because AI only sees <strong>the current file</strong>, not your system.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-20b4ec3cf08fe34b543225a941010c84" style="color:#0092eb">The “Constraint Header” Strategy</h2>



<p>Before any refactor, define rules:</p>



<p>Context:</p>



<ul class="wp-block-list">
<li>Strict types only</li>



<li>No direct DB calls (Repository pattern)</li>



<li>Async only via Amp</li>



<li>Immutable DTOs</li>
</ul>



<p>Task:<br>Refactor InvoiceGenerator</p>



<p>This reduces:</p>



<ul class="wp-block-list">
<li>random design decisions</li>



<li>pattern drift</li>



<li>“AI creativity” in critical code</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-e2434625c7e39488e10dc91b41944a9c" style="color:#0092eb">Killing the “God Constructor”</h1>



<p>Legacy PHP often looks like this:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function __construct(
    Logger $logger,
    Mailer $mailer,
    Cache $cache,
    PaymentGateway $gateway,
    ...
) {}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Logger</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$logger</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mailer</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$mailer</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$cache</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">PaymentGateway</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    ...</span></span>
<span class="line"><span style="color: #D4D4D4">) {}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>AI’s default move:<br>→ add another dependency</p>



<p>Wrong direction.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-74f1b508acd33839da4cea977502fe3d" style="color:#0092eb">Refactoring toward composition</h2>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>final class OrderProcessor
{
    public function __construct(
        private PaymentGateway $gateway,
        private Notifier $notifier,
    ) {}    public function process(Order $order): void
    {
        match ($order->status) {
            Status::Pending => $this->gateway->charge($order),
            Status::Paid => throw new AlreadyPaidException(),
            default => throw new \UnhandledMatchError(),
        };        $this->notifier->notify("Order {$order->id} processed");
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">final</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderProcessor</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PaymentGateway</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Notifier</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$notifier</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">process</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">match</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">status</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">Status</span><span style="color: #D4D4D4">::Pending </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">gateway</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">charge</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">Status</span><span style="color: #D4D4D4">::Paid </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">AlreadyPaidException</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">default</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> \</span><span style="color: #4EC9B0">UnhandledMatchError</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">        };        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">notifier</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">notify</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;Order {</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">} processed&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Key idea:</p>



<ul class="wp-block-list">
<li>fewer responsibilities per class</li>



<li>clearer boundaries</li>



<li>easier testing</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-4bf3f5dc1e18c8591b6d6cddcd35c30e" style="color:#0092eb">Hallucinated APIs: The “Ghost Methods” Problem</h1>



<p>AI sometimes invents functions that <em>sound correct</em>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>array_first_key($array); // ❌ does not exist</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #DCDCAA">array_first_key</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$array</span><span style="color: #D4D4D4">); </span><span style="color: #6A9955">// ❌ does not exist</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Rule:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>If you don’t recognize it, verify it.</p>
</blockquote>



<p>Ask:</p>



<ul class="wp-block-list">
<li>“Which PHP version introduced this?”</li>



<li>“Show official docs”</li>
</ul>



<p>No answer → hallucination.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-ee60b092621a61eda45ea8f3b600aea9" style="color:#0092eb">The Hidden Cost: AI-Grown Code</h1>



<p>If AI writes most of your refactoring:</p>



<ul class="wp-block-list">
<li>code becomes harder to reason about</li>



<li>intent disappears</li>



<li>onboarding slows down</li>
</ul>



<p>Countermeasure:</p>



<p>Ask AI:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“Explain why this design was chosen over a simpler alternative.”</p>
</blockquote>



<p>If the answer is vague → the design is weak.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-a82b51a14261027413293d4f18eda3ea" style="color:#0092eb">Reviewing AI Code Like a Senior</h1>



<p>Static analysis won’t save you.</p>



<p>Tools like PHPStan check correctness—not intent.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">What to actually review</h2>



<h3 class="wp-block-heading">1. Semantic correctness</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Did we simplify—or just move complexity?</p>
</blockquote>



<h3 class="wp-block-heading">2. Concurrency safety</h3>



<ul class="wp-block-list">
<li>hidden state mutation</li>



<li>ordering assumptions</li>



<li>error propagation</li>
</ul>



<h3 class="wp-block-heading">3. Edge cases</h3>



<p>Ask AI explicitly:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“Simulate a timeout during async execution. What breaks?”</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-a3b9fa0682ac22b34be5d812173a87ee" style="color:#0092eb">The 2026 Refactoring Checklist</h1>



<p>Before merging:</p>



<ul class="wp-block-list">
<li>Type safety → strict, explicit</li>



<li>No hidden shared state</li>



<li>Async is intentional (not accidental)</li>



<li>Dependencies are minimal and meaningful</li>



<li>Errors are explicit (no silent nulls)</li>



<li>You can explain every change</li>
</ul>



<p>If not:</p>



<p>→ don’t merge it</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-ba90e2b15a64cb8e9f81c922c5a402b6" style="color:#0092eb">Final Thoughts</h1>



<p>AI is an accelerator—but also a multiplier of mistakes.</p>



<p>It tends to:</p>



<ul class="wp-block-list">
<li>add complexity</li>



<li>over-engineer</li>



<li>hide intent</li>
</ul>



<p>Your role is no longer just writing code.</p>



<p>It’s <strong>curating it</strong>.</p>



<p>The real senior skill in 2026 is knowing when to say:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“This is too clever. Make it simple.”</p>
</blockquote>



<p>Because great refactoring isn’t about adding concurrency.</p>



<p>It’s about <strong>removing everything that doesn’t need to be there</strong>.</p>



<p></p>
<p>The post <a href="https://codecraftdiary.com/2026/03/21/ai-driven-refactoring/">AI-Driven Refactoring in PHP: When to Trust Copilot (and When to Take the Wheel)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/03/21/ai-driven-refactoring/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Small Pull Requests: Why They Move Development Teams Faster</title>
		<link>https://codecraftdiary.com/2026/03/15/small-pull-requests/</link>
					<comments>https://codecraftdiary.com/2026/03/15/small-pull-requests/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 15 Mar 2026 16:40:47 +0000</pubDate>
				<category><![CDATA[Development Workflow & Best Practices]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[software]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3203</guid>

					<description><![CDATA[<p>In many backend teams, pull requests slowly grow into something nobody wants to review. A developer starts working on a feature.At first, it’s just a small change — maybe a new endpoint or a service update. Soon another improvement gets added. A small refactor follows. Finally, a fix for something unrelated appears. Two days later, [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/03/15/small-pull-requests/">Small Pull Requests: Why They Move Development Teams Faster</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>In many backend teams, pull requests slowly grow into something nobody wants to review.</p>



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



<p>Soon another improvement gets added.</p>



<p>A small refactor follows.</p>



<p>Finally, a fix for something unrelated appears.</p>



<p>Two days later, the pull request contains <strong>900 lines of changes across 14 files</strong>.</p>



<p>At that moment, something predictable happens.</p>



<p>The review slows down.</p>



<p>Not because the reviewers are lazy — but because <strong>large pull requests create cognitive overload</strong>.</p>



<p>And once reviews slow down, the entire development workflow begins to lose momentum.</p>



<p>Previous article in this category: <a href="https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/">https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-1dc5a3ddc73656fc2039859e5c43350c" style="color:#0092eb">The Psychological Problem of Large Pull Requests</h2>



<p>Large pull requests create a subtle psychological effect.</p>



<p>When a reviewer opens a PR and sees:</p>



<pre class="wp-block-preformatted">+824 −217 changes</pre>



<p>their brain immediately categorizes it as <strong>expensive work</strong>.</p>



<p>The result is predictable:</p>



<ul class="wp-block-list">
<li>The review gets postponed.</li>



<li>The reviewer waits for “more time”.</li>



<li>The PR sits for hours or days.</li>
</ul>



<p>Eventually, when someone finally reviews it, they cannot realistically analyze everything in depth.</p>



<p>So one of two things happens:</p>



<ol class="wp-block-list">
<li>The reviewer skims the code and approves it quickly.</li>



<li>The reviewer leaves many comments that trigger long discussion threads.</li>
</ol>



<p>Neither outcome improves delivery speed.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-1f4cb7aca44a9d2329da87b1b6bc454f" style="color:#0092eb">Small Pull Requests Change the Dynamic</h2>



<p>Now compare that to a PR that looks like this:</p>



<pre class="wp-block-preformatted">+42 −8 changes</pre>



<p>This feels manageable.</p>



<p>A reviewer can realistically read the entire diff in a few minutes.</p>



<p>This leads to several important effects:</p>



<h3 class="wp-block-heading">1. Faster Reviews</h3>



<p>Small PRs get reviewed faster simply because they feel easier.</p>



<p>Many teams see review times drop from <strong>days to hours</strong> after adopting smaller pull requests.</p>



<h3 class="wp-block-heading">2. Better Feedback</h3>



<p>When the code is smaller, reviewers can focus on <strong>design decisions instead of scanning files</strong>.</p>



<p>Instead of writing comments like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“This file changed a lot, can you explain the logic?”</p>
</blockquote>



<p>they can ask more meaningful questions:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“Should this logic live in the service layer instead?”</p>
</blockquote>



<h3 class="wp-block-heading">3. Less Risk</h3>



<p>Large pull requests often hide bugs. </p>



<p>For example, reviewers may miss subtle mistakes when hundreds of lines change at once.</p>



<p>Small PRs isolate changes.</p>



<p>If a bug appears, it&#8217;s easier to trace it back to a specific change.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-a5f81b49293a51be4fd1ee67725476a3" style="color:#0092eb">A Practical Example</h2>



<p>Consider a backend developer implementing a new feature:</p>



<p><strong>Send notification emails when an order is shipped.</strong></p>



<p>A typical large PR might include:</p>



<ul class="wp-block-list">
<li>database migration</li>



<li>new notification service</li>



<li>email template</li>



<li>queue job</li>



<li>controller changes</li>



<li>unit tests</li>



<li>refactoring an old helper</li>
</ul>



<p>All combined into <strong>one big pull request</strong>.</p>



<p>Instead, this can be split into several smaller ones.</p>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-68d4d074756c8113331e5bb02c2f07da" style="color:#0092eb">PR 1 – Database Migration</h3>



<pre class="wp-block-preformatted">Add shipped_at column to orders table</pre>



<p>Simple schema change.</p>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-cd147756e198de2bf0ee729ca99516d7" style="color:#0092eb">PR 2 – Notification Service</h3>



<pre class="wp-block-preformatted">Create OrderShipmentNotifier service</pre>



<p>Pure backend logic.</p>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-d5b4e2b51b991185ee4f36643054f423" style="color:#0092eb">PR 3 – Queue Job</h3>



<pre class="wp-block-preformatted">Add SendShipmentEmail job</pre>



<p>Background processing.</p>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-3c9501aa132eb95b4441343d676dcf2f" style="color:#0092eb">PR 4 – Controller Integration</h3>



<pre class="wp-block-preformatted">Trigger notification when order is shipped</pre>



<p>Integration step.</p>



<p>Each PR becomes <strong>reviewable within minutes</strong>.</p>



<p>The entire feature moves through the pipeline <strong>much faster</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-2c6b94bf8523f873b5df89421b542611" style="color:#0092eb">The Hidden Benefit: Continuous Integration</h2>



<p>Small pull requests also improve CI pipelines.</p>



<p>Large PRs often cause several problems:</p>



<ul class="wp-block-list">
<li>longer build times</li>



<li>harder debugging when tests fail</li>



<li>multiple unrelated failures</li>
</ul>



<p>When a small PR fails CI, the cause is usually obvious.</p>



<p>Example:</p>



<pre class="wp-block-preformatted">PR title: Add OrderShipmentNotifier service<br>CI failure: NotificationServiceTest fails</pre>



<p>The fix is straightforward.</p>



<p>Compare this to a massive PR with dozens of changes — the failure may come from anywhere.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-5e076451f2c64bd8bebac87f3224c7ab" style="color:#0092eb">Why Developers Resist Small Pull Requests</h2>



<p>Despite the benefits, many developers hesitate to split their work.</p>



<p>Common reasons include:</p>



<h3 class="wp-block-heading">“The feature isn’t finished yet.”</h3>



<p>This is the most common argument.</p>



<p>But many PRs <strong>do not need to represent a finished feature</strong>.</p>



<p>They only need to represent a <strong>safe incremental step</strong>.</p>



<p>For example:</p>



<p>You can merge a service class before it&#8217;s used anywhere.</p>



<p>That’s perfectly valid.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">“It creates too many PRs.”</h3>



<p>This concern sounds logical but rarely holds in practice.</p>



<p>Ten small PRs usually move through the system faster than one large one.</p>



<p>Why?</p>



<p>Because multiple reviewers can process them quickly instead of blocking on one huge change.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">“It’s extra work.”</h3>



<p>At first, splitting work into smaller PRs requires discipline.</p>



<p>But after a few weeks, it becomes natural.</p>



<p>Developers start thinking in <strong>incremental changes</strong> rather than large feature drops.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-263f0f57fbdad1b056d82023092f9b1f" style="color:#0092eb">A Simple Rule That Works</h2>



<p>Some teams use a simple heuristic:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>If a pull request takes more than <strong>10 minutes to review</strong>, it is probably too large.</p>
</blockquote>



<p>This isn’t a strict rule, but it works surprisingly well.</p>



<p>Another guideline:</p>



<ul class="wp-block-list">
<li><strong>200–300 lines of diff</strong> should usually be the upper limit.</li>
</ul>



<p>Beyond that, review quality drops quickly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-9352b28be284fe8128f9ef469d5f9377" style="color:#0092eb">The Long-Term Impact on Workflow</h2>



<p>When teams consistently use small pull requests, several improvements appear over time.</p>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-6d0bb3bdb7a1846b3c6eb116fc39e0f7" style="color:#0092eb">The pipeline becomes predictable</h3>



<p>PRs move steadily through the system instead of forming large queues.</p>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-5e52a4f3df1c0fc971c68ad4564ee71f" style="color:#0092eb">Developers ship code more often</h3>



<p>Frequent merges reduce integration conflicts.</p>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-dd77dac8da84357d6d6a9c91b56ded35" style="color:#0092eb">Code reviews become collaborative</h3>



<p>Instead of being a bottleneck, reviews become quick feedback loops.</p>



<p>And perhaps most importantly:</p>



<p>The team starts focusing on <strong>continuous delivery instead of large feature dumps</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-6d323f610aeaac63c6a35ab35b774aa9" style="color:#0092eb">Final Thought</h2>



<p>Many teams try to improve their development workflow by introducing new tools, processes, or complex CI pipelines.</p>



<p>But one of the most effective improvements is surprisingly simple:</p>



<p><strong>Keep pull requests small.</strong></p>



<p>Small pull requests reduce friction in every stage of development:</p>



<ul class="wp-block-list">
<li>code review</li>



<li>testing</li>



<li>debugging</li>



<li>merging</li>
</ul>



<p>They allow teams to maintain momentum — and momentum is often the most valuable resource in software development.</p>



<p></p>
<p>The post <a href="https://codecraftdiary.com/2026/03/15/small-pull-requests/">Small Pull Requests: Why They Move Development Teams Faster</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/03/15/small-pull-requests/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Laravel Queue Testing: What Most Developers Get Wrong</title>
		<link>https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/</link>
					<comments>https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 08 Mar 2026 14:05:01 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3193</guid>

					<description><![CDATA[<p>Queues are where “it works on my machine” quietly turns into production incidents. Emails are sent in the background.Invoices are generated asynchronously.External APIs are called outside the request lifecycle.Data synchronization runs in workers you don’t actively watch. Laravel makes queues extremely easy to use. Add ShouldQueue, dispatch the job, done. Testing them properly is a [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/">Laravel Queue Testing: What Most Developers Get Wrong</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Queues are where “it works on my machine” quietly turns into production incidents.</p>



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



<p>Laravel makes queues extremely easy to use. Add <code>ShouldQueue</code>, dispatch the job, done.</p>



<p>Testing them properly is a different story.</p>



<p>Most developers stop at:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>Bus::fake();
Bus::assertDispatched(SyncOrderJob::class);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #4EC9B0">Bus</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #4EC9B0">Bus</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>That verifies wiring. It does <strong>not</strong> verify behavior.</p>



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



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Previous article in this category: <a href="https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/" type="link" id="https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/</a></p>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-f01b92696ee22741f1f5eee7c9ab5348" style="color:#0092eb">The Scenario: Syncing an Order to an External API</h1>



<p>Let’s say we have an <code>Order</code> model that needs to be synchronized to an external system after checkout.</p>



<h3 class="wp-block-heading">The Job</h3>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>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->order->synced_at !== null) {
            return; // idempotency guard
        }        $client->send(&#91;
            'id' => $this->order->id,
            'total' => $this->order->total,
        &#93;);        $this->order->update(&#91;
            'synced_at' => now(),
        &#93;);
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Jobs</span><span style="color: #D4D4D4">;</span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Models\</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Services\</span><span style="color: #4EC9B0">OrderApiClient</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Contracts\Queue\</span><span style="color: #4EC9B0">ShouldQueue</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Foundation\Bus\</span><span style="color: #4EC9B0">Dispatchable</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Queue\</span><span style="color: #4EC9B0">InteractsWithQueue</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Queue\</span><span style="color: #4EC9B0">SerializesModels</span><span style="color: #D4D4D4">;</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShouldQueue</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Dispatchable</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">InteractsWithQueue</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">SerializesModels</span><span style="color: #D4D4D4">;    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$tries</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">3</span><span style="color: #D4D4D4">;    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderApiClient</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$client</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">synced_at</span><span style="color: #D4D4D4"> !== </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4">; </span><span style="color: #6A9955">// idempotency guard</span></span>
<span class="line"><span style="color: #D4D4D4">        }        </span><span style="color: #9CDCFE">$client</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">send</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;total&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">total</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">update</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;synced_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Already we have several important concerns:</p>



<ul class="wp-block-list">
<li>The job must not sync twice.</li>



<li>It depends on an external API client.</li>



<li>It may be retried.</li>



<li>It mutates persistent state.</li>
</ul>



<p>Let’s test it properly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-68a7269f5277a3d9981457cb726ca810" style="color:#0092eb">1. Testing Dispatch (Integration Level)</h1>



<p>This belongs in a feature test.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>use Illuminate\Support\Facades\Bus;
use App\Jobs\SyncOrderJob;

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

    $order = Order::factory()->create();

    $order->markAsPaid(); // imagine this dispatches the job

    Bus::assertDispatched(SyncOrderJob::class, function ($job) use ($order) {
        return $job->order->is($order);
    });
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Support\Facades\</span><span style="color: #4EC9B0">Bus</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Jobs\</span><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_order_dispatches_sync_job</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Bus</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">markAsPaid</span><span style="color: #D4D4D4">(); </span><span style="color: #6A9955">// imagine this dispatches the job</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Bus</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4">) </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">is</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    });</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Good. Necessary.</p>



<p>But insufficient.</p>



<p>This does not test <code>handle()</code> at all.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-7db775fc253db3e38ffc33ff8abe832d" style="color:#0092eb">2. Testing Job Logic Directly (Unit Level)</h1>



<p>Now we test the job itself.</p>



<p>We do <strong>not</strong> fake the bus.<br>We instantiate the job and call <code>handle()</code> directly.</p>



<h2 class="wp-block-heading">Mocking the External API</h2>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>use App\Services\OrderApiClient;
use Mockery;

public function test_it_calls_external_api_when_not_synced()
{
    $order = Order::factory()->create(&#91;
        'synced_at' => null,
    &#93;);    
    
    $mock = Mockery::mock(OrderApiClient::class);
    
    $mock->shouldReceive('send')
        ->once()
        ->with(Mockery::on(fn ($payload) => $payload&#91;'id'&#93; === $order->id));    
    
    $job = new SyncOrderJob($order);    
    
    $job->handle($mock);
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Services\</span><span style="color: #4EC9B0">OrderApiClient</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Mockery</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_it_calls_external_api_when_not_synced</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;synced_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$mock</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Mockery</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">mock</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderApiClient</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$mock</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">shouldReceive</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;send&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">once</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">with</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Mockery</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">on</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">fn</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$payload</span><span style="color: #D4D4D4">) =&gt; </span><span style="color: #9CDCFE">$payload</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4">&#93; === </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">));    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$mock</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>This verifies real behavior.</p>



<p>We are not testing Laravel.<br>We are testing our business logic.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-9636cc6204f12044f990637fee7b9878" style="color:#0092eb">3. Testing Idempotency</h1>



<p>Queues retry automatically. If your job is not idempotent, you will create duplicate side effects.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function test_it_does_not_call_api_if_already_synced()
{
    $order = Order::factory()->create(&#91;
        'synced_at' => now(),
    &#93;);    
    
    $mock = Mockery::mock(OrderApiClient::class);
    
    $mock->shouldNotReceive('send');    
    
    $job = new SyncOrderJob($order);    
    
    $job->handle($mock);
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_it_does_not_call_api_if_already_synced</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;synced_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$mock</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Mockery</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">mock</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderApiClient</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$mock</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">shouldNotReceive</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;send&#39;</span><span style="color: #D4D4D4">);    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$mock</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>If this test fails, your production system will eventually duplicate work.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-976e8476069906cee1afb3c5993bbd5f" style="color:#0092eb">4. Testing Retry Safety</h1>



<p>Consider a failure between the API call and the database update.</p>



<p>If the job crashes after calling the API but before marking the order as synced, retry will send it again.</p>



<p>Safer pattern:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function handle(OrderApiClient $client): void
{
    if ($this->order->synced_at !== null) {
        return;
    }    $this->order->update(&#91;
        'synced_at' => now(),
    &#93;);    $client->send(&#91;
        'id' => $this->order->id,
        'total' => $this->order->total,
    &#93;);
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderApiClient</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$client</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">synced_at</span><span style="color: #D4D4D4"> !== </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">update</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;synced_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);    </span><span style="color: #9CDCFE">$client</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">send</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;total&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">total</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Now state changes before side effects.</p>



<p>Let’s simulate a failure.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function test_it_retries_safely()
{
    $order = Order::factory()->create(&#91;
        'synced_at' => null,
    &#93;);

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

    $job = new SyncOrderJob($order);

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

    $order->refresh();

    $this->assertNotNull($order->synced_at);
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_it_retries_safely</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;synced_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$mock</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Mockery</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">mock</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderApiClient</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$mock</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">shouldReceive</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;send&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">once</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">andThrow</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">RuntimeException</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">try</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$mock</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    } </span><span style="color: #C586C0">catch</span><span style="color: #D4D4D4"> (</span><span style="color: #4EC9B0">RuntimeException</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$e</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// expected</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">refresh</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertNotNull</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">synced_at</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Now even if retry happens, the idempotency guard prevents double execution.</p>



<p>This is not theoretical. This is how duplicate invoices happen.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-1c1983ba14aa10c5700df1779bedb764" style="color:#0092eb">5. Testing Jobs That Dispatch Other Jobs</h1>



<p>Chained or nested jobs add complexity.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>ProcessPaymentJob::dispatch($order);
SendInvoiceJob::dispatch($order);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #4EC9B0">ProcessPaymentJob</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #4EC9B0">SendInvoiceJob</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>To test that correctly:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>Bus::fake();

$job = new ProcessPaymentJob($order);

$job->handle($paymentService);

Bus::assertDispatched(SendInvoiceJob::class);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #4EC9B0">Bus</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ProcessPaymentJob</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$paymentService</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #4EC9B0">Bus</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">SendInvoiceJob</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Notice the difference:</p>



<ul class="wp-block-list">
<li>We test <code>handle()</code> directly.</li>



<li>We fake the bus only to assert nested dispatch.</li>
</ul>



<p>That separation is critical.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-ece9b4b0c89c060ae167021c86d0a922" style="color:#0092eb">6. Testing Backoff and Retry Configuration</h1>



<p>Laravel allows configuration:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public int $tries = 5;

public function backoff(): array
{
    return &#91;10, 30, 60&#93;;
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$tries</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">backoff</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">array</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> &#91;</span><span style="color: #B5CEA8">10</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">30</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">60</span><span style="color: #D4D4D4">&#93;;</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>You can test configuration explicitly:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function test_job_has_correct_retry_settings()
{
    $job = new SyncOrderJob(Order::factory()->make());

    $this->assertEquals(3, $job->tries);
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_job_has_correct_retry_settings</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">make</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertEquals</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">3</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$job</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">tries</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Not glamorous — but configuration errors cause real outages.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-bc824be390e9fed77dee306138b9e192" style="color:#0092eb">7. The Dangerous Anti-Pattern</h1>



<p>This is common:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>Queue::fake();

SyncOrderJob::dispatch($order);

Queue::assertPushed(SyncOrderJob::class);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">SyncOrderJob</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>This test will pass even if:</p>



<ul class="wp-block-list">
<li>The job throws immediately.</li>



<li>The API client is broken.</li>



<li>The logic is inverted.</li>



<li>Idempotency is missing.</li>



<li>The job deletes the order by accident.</li>
</ul>



<p>You tested dispatch, not behavior.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-8b4cf6d303308b37c5fee22666da5a11" style="color:#0092eb">When to Use <code>Bus::fake()</code> vs Real Execution</h1>



<p>Use <code>Bus::fake()</code> when:</p>



<ul class="wp-block-list">
<li>Testing controllers or services that dispatch jobs.</li>



<li>Verifying orchestration.</li>



<li>Ensuring a job is queued under certain conditions.</li>
</ul>



<p>Do <strong>not</strong> use <code>Bus::fake()</code> when:</p>



<ul class="wp-block-list">
<li>Testing job business logic.</li>



<li>Testing external integration behavior.</li>



<li>Testing failure handling.</li>
</ul>



<p>In job tests, instantiate and call <code>handle()</code> directly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-08be63123854c36fe84e03325cdf5731" style="color:#0092eb">Advanced: Running Jobs Synchronously in Tests</h1>



<p>Laravel allows:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>config(&#91;'queue.default' => 'sync'&#93;);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #DCDCAA">config</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;queue.default&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;sync&#39;</span><span style="color: #D4D4D4">&#93;);</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>This executes jobs immediately.</p>



<p>Useful for feature tests where:</p>



<ul class="wp-block-list">
<li>You want full behavior executed.</li>



<li>You don’t care about queue infrastructure.</li>
</ul>



<p>But be careful:</p>



<p>If you rely on sync execution everywhere, you may miss race conditions or retry issues.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-bece34793fa3c27fa61eec730c2d6db8" style="color:#0092eb">Design Principles for Testable Jobs</h1>



<p>If your job is hard to test, it’s usually poorly designed.</p>



<p>Good Laravel jobs:</p>



<ul class="wp-block-list">
<li>Inject services via <code>handle()</code>.</li>



<li>Keep constructors lightweight.</li>



<li>Avoid heavy logic in <code>__construct</code>.</li>



<li>Avoid static service calls.</li>



<li>Are idempotent by design.</li>



<li>Fail loudly.</li>
</ul>



<p>Bad Laravel jobs:</p>



<ul class="wp-block-list">
<li>Call <code>Http::post()</code> directly inside handle.</li>



<li>Query models statically without abstraction.</li>



<li>Mutate global state.</li>



<li>Swallow exceptions silently.</li>
</ul>



<p>Testing reveals architecture quality.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-c5432b6bce646f52c44b4deab04349f2" style="color:#0092eb">Final Thoughts</h1>



<p>Queues introduce three new axes of complexity:</p>



<ol class="wp-block-list">
<li>Time (execution is delayed)</li>



<li>Failure (automatic retries)</li>



<li>Concurrency (multiple workers)</li>
</ol>



<p>Testing Laravel jobs properly means:</p>



<ul class="wp-block-list">
<li>Testing dispatch at feature level.</li>



<li>Testing <code>handle()</code> directly at unit level.</li>



<li>Mocking external services.</li>



<li>Verifying idempotency.</li>



<li>Simulating failure.</li>



<li>Thinking explicitly about retry safety.</li>
</ul>



<p>Queues scale your application.</p>



<p>Tests make your background processing reliable.</p>



<p></p>
<p>The post <a href="https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/">Laravel Queue Testing: What Most Developers Get Wrong</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Singleton Pattern in PHP: Refactoring Global State the Right Way</title>
		<link>https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/</link>
					<comments>https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 28 Feb 2026 11:54:19 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<category><![CDATA[software-design]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3189</guid>

					<description><![CDATA[<p>In many legacy PHP codebases, global state sneaks in quietly.A config.php file is included everywhere.A static helper class grows until it becomes a god object.A database connection lives in a global variable “just for now”. At first, it feels convenient.Later, it becomes untestable, unpredictable, and painful to change. Let’s look at how global state often [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/">Singleton Pattern in PHP: Refactoring Global State the Right Way</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>In many legacy PHP codebases, global state sneaks in quietly.<br>A <code>config.php</code> file is included everywhere.<br>A static helper class grows until it becomes a god object.<br>A database connection lives in a global variable “just for now”.</p>



<p>At first, it feels convenient.<br>Later, it becomes untestable, unpredictable, and painful to change.</p>



<p>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.</p>



<p>Previously article in this category: <a href="https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-a2e3bdd4caf7dd3414bd5d19896872c7" style="color:#0092eb">The Code Smell: Hidden Global State</h2>



<p>A typical example:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// config.php
return &#91;
    'db_host' => 'localhost',
    'db_user' => 'root',
    'db_pass' => 'secret',
&#93;;</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #6A9955">// config.php</span></span>
<span class="line"><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;db_host&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;localhost&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;db_user&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;root&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;db_pass&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;secret&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">&#93;;</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// bootstrap.php
$config = require 'config.php';</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #6A9955">// bootstrap.php</span></span>
<span class="line"><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4"> = </span><span style="color: #C586C0">require</span><span style="color: #D4D4D4"> </span><span style="color: #CE9178">&#39;config.php&#39;</span><span style="color: #D4D4D4">;</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// UserRepository.php
class UserRepository
{
    public function find(int $id): array
    {
        global $config;        $conn = new PDO(
            'mysql:host=' . $config&#91;'db_host'&#93;,
            $config&#91;'db_user'&#93;,
            $config&#91;'db_pass'&#93;
        );        return $conn->query('SELECT * FROM users WHERE id = ' . $id)->fetch();
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #6A9955">// UserRepository.php</span></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">UserRepository</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">array</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">global</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">;        </span><span style="color: #9CDCFE">$conn</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PDO</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;mysql:host=&#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;db_host&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;db_user&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;db_pass&#39;</span><span style="color: #D4D4D4">&#93;</span></span>
<span class="line"><span style="color: #D4D4D4">        );        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$conn</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">query</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;</span><span style="color: #569CD6">SELECT</span><span style="color: #CE9178"> </span><span style="color: #D4D4D4">*</span><span style="color: #CE9178"> </span><span style="color: #569CD6">FROM</span><span style="color: #CE9178"> users </span><span style="color: #569CD6">WHERE</span><span style="color: #CE9178"> id </span><span style="color: #D4D4D4">=</span><span style="color: #CE9178"> &#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">fetch</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>This code “works”.<br>It also hides a dependency.</p>



<p><code>UserRepository</code> depends on configuration and a database connection, but nothing in its API communicates that. The dependency is invisible and implicit.</p>



<p>This leads to:</p>



<ul class="wp-block-list">
<li>unpredictable behavior in tests</li>



<li>coupling between unrelated parts of the system</li>



<li>accidental mutation of shared state</li>



<li>code that is hard to reason about</li>
</ul>



<p>This is a classic code smell: <strong>hidden dependencies via global state</strong>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-302328d17bb5d6dfdf666d8bee242020" style="color:#0092eb">The Naive Fix: Static Helper Class</h2>



<p>The next evolutionary step in many PHP projects looks like this:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>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&#91;$key&#93;;
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Config</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">;    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">load</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::</span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4"> = </span><span style="color: #C586C0">require</span><span style="color: #D4D4D4"> </span><span style="color: #CE9178">&#39;config.php&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$key</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">mixed</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::</span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #9CDCFE">$key</span><span style="color: #D4D4D4">&#93;;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>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->query('SELECT * FROM users WHERE id = ' . $id)->fetch();
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">UserRepository</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">array</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$conn</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PDO</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;mysql:host=&#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Config</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;db_host&#39;</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">Config</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;db_user&#39;</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">Config</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;db_pass&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        );        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$conn</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">query</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;</span><span style="color: #569CD6">SELECT</span><span style="color: #CE9178"> </span><span style="color: #D4D4D4">*</span><span style="color: #CE9178"> </span><span style="color: #569CD6">FROM</span><span style="color: #CE9178"> users </span><span style="color: #569CD6">WHERE</span><span style="color: #CE9178"> id </span><span style="color: #D4D4D4">=</span><span style="color: #CE9178"> &#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">fetch</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>This looks cleaner:</p>



<ul class="wp-block-list">
<li>no <code>global</code></li>



<li>no messy includes</li>



<li>centralized config access</li>
</ul>



<p>But architecturally, nothing really improved.</p>



<p>This is still:</p>



<ul class="wp-block-list">
<li>global state</li>



<li>hidden dependency</li>



<li>impossible to replace in tests</li>



<li>tightly coupled to a static API</li>
</ul>



<p>We replaced <code>global</code> with a static singleton-like façade.</p>



<p>This is not refactoring.<br>This is just changing syntax.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-65e77bb3d9eb747443679b4c9df8d652" style="color:#0092eb">Refactoring Toward a Real Singleton</h2>



<p>Let’s refactor this step by step into a proper Singleton.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>final class Config
{
    private static ?Config $instance = null;
    private array $data;    private function __construct()
    {
        $this->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->data&#91;$key&#93; ?? null;
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">final</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Config</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> ?</span><span style="color: #4EC9B0">Config</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$instance</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">;    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">data</span><span style="color: #D4D4D4"> = </span><span style="color: #C586C0">require</span><span style="color: #D4D4D4"> </span><span style="color: #CE9178">&#39;config.php&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">getInstance</span><span style="color: #D4D4D4">(): </span><span style="color: #4EC9B0">Config</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::</span><span style="color: #9CDCFE">$instance</span><span style="color: #D4D4D4"> === </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::</span><span style="color: #9CDCFE">$instance</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Config</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">        }        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::</span><span style="color: #9CDCFE">$instance</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$key</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">mixed</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #9CDCFE">$key</span><span style="color: #D4D4D4">&#93; ?? </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Usage:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class UserRepository
{
    public function find(int $id): array
    {
        $config = Config::getInstance();        $conn = new PDO(
            'mysql:host=' . $config->get('db_host'),
            $config->get('db_user'),
            $config->get('db_pass')
        );        return $conn->query('SELECT * FROM users WHERE id = ' . $id)->fetch();
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">UserRepository</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">array</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Config</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">getInstance</span><span style="color: #D4D4D4">();        </span><span style="color: #9CDCFE">$conn</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PDO</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;mysql:host=&#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;db_host&#39;</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;db_user&#39;</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;db_pass&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        );        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$conn</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">query</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;</span><span style="color: #569CD6">SELECT</span><span style="color: #CE9178"> </span><span style="color: #D4D4D4">*</span><span style="color: #CE9178"> </span><span style="color: #569CD6">FROM</span><span style="color: #CE9178"> users </span><span style="color: #569CD6">WHERE</span><span style="color: #CE9178"> id </span><span style="color: #D4D4D4">=</span><span style="color: #CE9178"> &#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">fetch</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>This is now a “proper” Singleton:</p>



<ul class="wp-block-list">
<li>private constructor</li>



<li>lazy initialization</li>



<li>controlled access point</li>
</ul>



<p>We improved:</p>



<ul class="wp-block-list">
<li>structure</li>



<li>encapsulation</li>



<li>testability (slightly)</li>
</ul>



<p>But we did <strong>not</strong> fix the core design issue.</p>



<p>The dependency is still hidden.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-5d0373849eb44befb940fccb7a531018" style="color:#0092eb">The Real Problem: Singleton Is Global State in Disguise</h2>



<p>A Singleton is not dependency injection.<br>It is <strong>global state with better manners</strong>.</p>



<p>You still cannot:</p>



<ul class="wp-block-list">
<li>pass different configurations in tests</li>



<li>swap implementations easily</li>



<li>reason locally about dependencies</li>
</ul>



<p>From an architectural perspective, <code>UserRepository</code> still depends on <code>Config</code>, but that dependency is invisible in its constructor or method signature.</p>



<p>This creates tight coupling and invisible control flow.</p>



<p>The code is cleaner — but not better designed.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-472c2c10e3b1fdae1430287b1b045b5f" style="color:#0092eb">When Singleton Is Actually Legitimate</h2>



<p>Singleton is not evil by definition.<br>It is evil when used as a default container for everything.</p>



<p>Legitimate use cases in PHP:</p>



<h3 class="wp-block-heading">1. Immutable Application Configuration</h3>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>final class AppConfig
{
    private static ?AppConfig $instance = null;
    private array $values;    private function __construct(array $values)
    {
        $this->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->values&#91;$key&#93; ?? null;
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">final</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">AppConfig</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> ?</span><span style="color: #4EC9B0">AppConfig</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$instance</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$values</span><span style="color: #D4D4D4">;    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$values</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">values</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$values</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">boot</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$values</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::</span><span style="color: #9CDCFE">$instance</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$values</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">getInstance</span><span style="color: #D4D4D4">(): </span><span style="color: #4EC9B0">AppConfig</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::</span><span style="color: #9CDCFE">$instance</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$key</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">mixed</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">values</span><span style="color: #D4D4D4">&#91;</span><span style="color: #9CDCFE">$key</span><span style="color: #D4D4D4">&#93; ?? </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Bootstrapped once, read-only afterwards.</p>



<h3 class="wp-block-heading">2. Logger</h3>



<p>A logger is often a cross-cutting concern:</p>



<ul class="wp-block-list">
<li>shared</li>



<li>stateless</li>



<li>global by nature</li>
</ul>



<h3 class="wp-block-heading">3. Feature Flags / Environment Context</h3>



<p>If the state is:</p>



<ul class="wp-block-list">
<li>immutable</li>



<li>read-only</li>



<li>environment-level</li>
</ul>



<p>Singleton can be acceptable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-0807606d3744547da2f2e88582690944" style="color:#0092eb">When Singleton Is a Design Smell</h2>



<p>Singleton becomes a problem when:</p>



<ul class="wp-block-list">
<li>it contains mutable domain state</li>



<li>it replaces proper dependency injection</li>



<li>it hides real architectural boundaries</li>



<li>it becomes a service locator</li>



<li>business logic starts living inside it</li>
</ul>



<p>If your Singleton has:</p>



<ul class="wp-block-list">
<li>20 methods</li>



<li>multiple responsibilities</li>



<li>domain logic</li>
</ul>



<p>…you created a god object with a private constructor.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-c239d5d05a8679881e83f144023d3fe2" style="color:#0092eb">A Better Refactoring Path</h2>



<p>Often, Singleton should be treated as a <strong>transitional refactoring step</strong>, not a final architecture.</p>



<p>Better end state:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class UserRepository
{
    public function __construct(
        private PDO $connection
    ) {}    public function find(int $id): array
    {
        return $this->connection
            ->query('SELECT * FROM users WHERE id = ' . $id)
            ->fetch();
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">UserRepository</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PDO</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$connection</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">array</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">connection</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">query</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;</span><span style="color: #569CD6">SELECT</span><span style="color: #CE9178"> </span><span style="color: #D4D4D4">*</span><span style="color: #CE9178"> </span><span style="color: #569CD6">FROM</span><span style="color: #CE9178"> users </span><span style="color: #569CD6">WHERE</span><span style="color: #CE9178"> id </span><span style="color: #D4D4D4">=</span><span style="color: #CE9178"> &#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">fetch</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>And composition happens at the boundary of the application:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$config = Config::getInstance();$pdo = new PDO(
    'mysql:host=' . $config->get('db_host'),
    $config->get('db_user'),
    $config->get('db_pass')
);$repo = new UserRepository($pdo);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Config</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">getInstance</span><span style="color: #D4D4D4">();</span><span style="color: #9CDCFE">$pdo</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PDO</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;mysql:host=&#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;db_host&#39;</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;db_user&#39;</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$config</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;db_pass&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">);</span><span style="color: #9CDCFE">$repo</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">UserRepository</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$pdo</span><span style="color: #D4D4D4">);</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Now:</p>



<ul class="wp-block-list">
<li>dependencies are explicit</li>



<li>testing is trivial</li>



<li>coupling is controlled</li>



<li>architecture is visible</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-038b0461bd9aae196b30f81412f4a54f" style="color:#0092eb">Conclusion</h2>



<p>Singleton is not a goal.<br>It is a compromise.</p>



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



<p>But if you stop there, you only replaced chaos with a nicer-looking global variable.</p>



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



<p>Singleton is a tool.<br>Use it deliberately — and sparingly.</p>
<p>The post <a href="https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/">Singleton Pattern in PHP: Refactoring Global State the Right Way</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Why “Just One More Quick Fix” Destroys Your Delivery Pipeline</title>
		<link>https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/</link>
					<comments>https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 21 Feb 2026 10:40:24 +0000</pubDate>
				<category><![CDATA[Development Workflow & Best Practices]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[software-design]]></category>
		<category><![CDATA[workflow]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3185</guid>

					<description><![CDATA[<p>Every development team has heard this sentence: “Let’s just push one more quick fix.” 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. [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/">Why “Just One More Quick Fix” Destroys Your Delivery Pipeline</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Every development team has heard this sentence:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“Let’s just push one more quick fix.”</p>
</blockquote>



<p>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.</p>



<p>The problem is not the quick fix itself.<br>The problem is the <em>culture</em> that grows around it.</p>



<p>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.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Previous article in this category: <a href="https://codecraftdiary.com/2026/01/31/why-almost-done-work-breaks-development-flow/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/01/31/why-almost-done-work-breaks-development-flow/</a></p>



<h2 class="wp-block-heading">How Quick Fixes Become the Normal Path to Production</h2>



<p>Most teams don’t start with bad intentions. The first few quick fixes usually happen under real pressure:</p>



<ul class="wp-block-list">
<li>A critical bug blocks a customer.</li>



<li>A payment fails.</li>



<li>An integration with a third-party API breaks overnight.</li>



<li>A configuration mistake takes down part of production.</li>
</ul>



<p>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.</p>



<p>Nothing explodes. The fix works. The customer is happy. The team moves on.</p>



<p>That success teaches the wrong lesson.</p>



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



<p>After a few months, you end up with two delivery paths:</p>



<ul class="wp-block-list">
<li>The <strong>official pipeline</strong>: PR → review → tests → CI → deploy</li>



<li>The <strong>real pipeline</strong>: Slack message → quick commit → direct deploy</li>
</ul>



<p>The second one becomes the faster, socially accepted option. The first one starts to feel like bureaucracy.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The Hidden Damage: You Don’t See the Cost Immediately</h2>



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



<h3 class="wp-block-heading">1. Your CI Pipeline Stops Being Trustworthy</h3>



<p>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.</p>



<p>Eventually, developers stop trusting the pipeline at all.</p>



<p>At that point, CI is no longer a quality gate — it’s just a ritual.<br>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.</p>



<h3 class="wp-block-heading">2. Code Reviews Become Optional (Which Means Useless)</h3>



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



<p>That’s a subtle but dangerous shift.</p>



<p>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.</p>



<h3 class="wp-block-heading">3. You Normalize Risk Without Noticing</h3>



<p>Every quick fix that works without immediate consequences normalizes risk-taking:</p>



<ul class="wp-block-list">
<li>Deploying without review becomes acceptable.</li>



<li>Deploying without tests becomes acceptable.</li>



<li>Deploying without understanding side effects becomes acceptable.</li>
</ul>



<p>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.</p>



<p>By the time that incident happens, the delivery pipeline is already too eroded to protect you.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The Psychological Trap: Quick Fixes Feel Like Ownership</h2>



<p>Quick fixes often feel good on a personal level.</p>



<p>You fixed a production issue fast.<br>You unblocked a customer.<br>You were the hero who saved the day.</p>



<p>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.</p>



<p>Over time, teams drift into a culture where:</p>



<ul class="wp-block-list">
<li>Being fast matters more than being safe.</li>



<li>Fixing symptoms is rewarded more than fixing root causes.</li>



<li>Short-term relief beats long-term stability.</li>
</ul>



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



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Real-World Pattern: The “Temporary” Hotfix That Becomes Permanent</h2>



<p>A common pattern looks like this:</p>



<ol class="wp-block-list">
<li>A bug appears in production.</li>



<li>Someone applies a quick fix directly in production or through a rushed commit.</li>



<li>The fix is labeled “temporary.”</li>



<li>The team plans to clean it up later.</li>



<li>Later never comes.</li>
</ol>



<p>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.</p>



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



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Why This Destroys Flow (Not Just Quality)</h2>



<p>Quick fixes don’t just hurt quality. They destroy delivery flow.</p>



<p>Once bypassing the pipeline becomes normal:</p>



<ul class="wp-block-list">
<li>Developers hesitate to touch fragile areas.</li>



<li>Releases become unpredictable.</li>



<li>Small changes cause big side effects.</li>



<li>People slow down because they don’t trust the system.</li>
</ul>



<p>Ironically, the behavior that started as “moving faster” ends up making delivery slower and more stressful.</p>



<p>You trade short-term speed for long-term friction.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">How to Fix the Culture Without Becoming Bureaucratic</h2>



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



<h3 class="wp-block-heading">1. Make the Fast Path the Safe Path</h3>



<p>If your CI takes 40 minutes, people will bypass it.<br>If your review process blocks urgent fixes, people will bypass it.</p>



<p>Your pipeline must be optimized for speed in emergencies:</p>



<ul class="wp-block-list">
<li>Lightweight reviews for hotfixes.</li>



<li>Fast CI path for critical changes.</li>



<li>Clear “hotfix” workflow that still enforces minimal quality gates.</li>
</ul>



<h3 class="wp-block-heading">2. Track Bypasses as Technical Debt</h3>



<p>Every bypass should leave a visible trace:</p>



<ul class="wp-block-list">
<li>A ticket to add tests later.</li>



<li>A follow-up task to clean up the quick fix.</li>



<li>A post-incident note explaining what was skipped.</li>
</ul>



<p>If bypassing the pipeline leaves no trace, it becomes invisible debt.</p>



<h3 class="wp-block-heading">3. Reward Stability, Not Heroics</h3>



<p>Publicly recognize:</p>



<ul class="wp-block-list">
<li>Fixes that go through the proper pipeline under pressure.</li>



<li>Improvements to CI speed.</li>



<li>Reductions in flaky tests.</li>
</ul>



<p>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.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The Core Insight</h2>



<p>“Just one more quick fix” is never just one.</p>



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



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



<p>You have hope and heroics.</p>



<p>And that’s not a strategy.</p>



<p></p>
<p>The post <a href="https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/">Why “Just One More Quick Fix” Destroys Your Delivery Pipeline</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Contract Testing External APIs in PHP with Pact (Real Laravel Example)</title>
		<link>https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/</link>
					<comments>https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 14 Feb 2026 12:43:26 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[api]]></category>
		<category><![CDATA[mocking]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[software-design]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3173</guid>

					<description><![CDATA[<p>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. I learned this the hard way. In one project, all my tests were green. My [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/">Contract Testing External APIs in PHP with Pact (Real Laravel Example)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>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.</p>



<p>I learned this the hard way.</p>



<p>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.</p>



<p>Mocks told me everything was fine. Reality disagreed.</p>



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



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-61b2144564a5c21db7dba69781163bea" style="color:#0092eb">Why Mocking External APIs Is Not Enough</h2>



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



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>They only test your assumptions about the API, not the API itself.</p>
</blockquote>



<p>Typical problems I’ve seen in production:</p>



<ul class="wp-block-list">
<li>The API removes or renames a field.</li>



<li>A nested structure changes shape.</li>



<li>The API starts returning <code>404</code> instead of <code>200</code> in some edge cases.</li>



<li>The API adds a required field to the request body.</li>
</ul>



<p>Your mocks will never catch this unless you manually update them. That means mocks alone can silently drift away from reality.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-38cc5459dc519e6c7735663aba2991d1" style="color:#0092eb">What Is Contract Testing (In Plain English)</h2>



<p>Contract testing sits between mocking and full integration tests.</p>



<p>Instead of saying:</p>



<blockquote class="wp-block-quote is-style-plain is-layout-flow wp-block-quote-is-layout-flow">
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“This is what I think the API returns,”</p>
</blockquote>
</blockquote>



<p>you say:</p>



<blockquote class="wp-block-quote is-style-plain is-layout-flow wp-block-quote-is-layout-flow">
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“This is the contract between my app (consumer) and the API (provider).”</p>
</blockquote>
</blockquote>



<p>With consumer-driven contract testing:</p>



<ul class="wp-block-list">
<li>The <strong>consumer</strong> defines what it expects from the API.</li>



<li>The <strong>provider</strong> must verify that it actually fulfills this contract.</li>
</ul>



<p>If the provider changes something that breaks the contract, tests fail <strong>before production breaks</strong>.</p>



<p>This turns breaking API changes from a runtime surprise into a build-time failure.</p>



<p><em>Diagram: How contract testing works between a Laravel consumer, Pact mock server, and the real API provider.</em></p>



<figure class="wp-block-image size-large is-resized"><img fetchpriority="high" decoding="async" width="1024" height="683" src="https://codecraftdiary.com/wp-content/uploads/2026/02/Contract-testing-flow-diagram-1024x683.webp" alt="" class="wp-image-3179" style="aspect-ratio:1.499297204435421;width:699px;height:auto" srcset="https://codecraftdiary.com/wp-content/uploads/2026/02/Contract-testing-flow-diagram-1024x683.webp 1024w, https://codecraftdiary.com/wp-content/uploads/2026/02/Contract-testing-flow-diagram-300x200.webp 300w, https://codecraftdiary.com/wp-content/uploads/2026/02/Contract-testing-flow-diagram-768x512.webp 768w, https://codecraftdiary.com/wp-content/uploads/2026/02/Contract-testing-flow-diagram.webp 1536w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-e55492e0a7790218d689e747fb632e40" style="color:#0092eb">Real-World Scenario: Laravel as API Consumer</h2>



<p>Let’s assume a Laravel application integrates with an external billing API:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>GET /api/customers/{id}

Response:
{
  "id": 123,
  "email": "john@example.com",
  "is_active": true
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">GET /api/customers/{</span><span style="color: #F44747">id</span><span style="color: #D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">Response:</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE">&quot;id&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE">&quot;email&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;john@example.com&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE">&quot;is_active&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #569CD6">true</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">JSON</span></div>



<p>Your Laravel service:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class BillingClient
{
    public function getCustomer(int $id): array
    {
        $response = Http::get("https://billing.example.com/api/customers/{$id}");

        return $response->json();
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">BillingClient</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">getCustomer</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">array</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;https://billing.example.com/api/customers/{</span><span style="color: #9CDCFE">$id</span><span style="color: #CE9178">}&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Your application relies on:</p>



<ul class="wp-block-list">
<li><code>email</code></li>



<li><code>is_active</code></li>
</ul>



<p>If the provider removes <code>is_active</code>, your app breaks. Mocks won’t catch it. Contract testing will.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-628d444445d134f8756184e26e786e2c" style="color:#0092eb">Step-by-Step: Contract Testing with Pact in Laravel</h2>



<h3 class="wp-block-heading has-palette-color-4-color has-text-color has-link-color wp-elements-f6b58a27bb890bafae8ed296433819c7">Installing Pact for PHP</h3>



<p>Install Pact dependencies for your test environment:</p>



<pre class="wp-block-code"><code>composer require --dev pact-foundation/pact-php</code></pre>



<p>You’ll also need the Pact CLI running locally or in CI (usually via Docker).</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-88fbf33d542bf83ad10aa7446ff24ea6" style="color:#0092eb">Writing the Consumer Contract Test</h3>



<p>The goal: define what your Laravel app <strong>expects</strong> from the API.</p>



<p>Example consumer test:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>public function test_it_fetches_customer_from_billing_api()
{
    $builder = new InteractionBuilder();
    
    $builder
        ->given('Customer 123 exists')
        ->uponReceiving('A request for customer 123')
        ->with(&#91;
            'method' => 'GET',
            'path'   => '/api/customers/123',
        &#93;)
        ->willRespondWith([
            'status' => 200,
            'headers' => &#91;'Content-Type' => 'application/json'&#93;,
            'body' => &#91;
                'id' => 123,
                'email' => 'john@example.com',
                'is_active' => true,
            &#93;,
        ]);

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

        $this->assertSame('john@example.com', $customer&#91;'email'&#93;);
        $this->assertTrue($customer&#91;'is_active'&#93;);
    });
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_it_fetches_customer_from_billing_api</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$builder</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">InteractionBuilder</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$builder</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">given</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Customer 123 exists&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">uponReceiving</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;A request for customer 123&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">with</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;method&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;GET&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;path&#39;</span><span style="color: #D4D4D4">   =&gt; </span><span style="color: #CE9178">&#39;/api/customers/123&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">willRespondWith</span><span style="color: #D4D4D4">([</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">200</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;headers&#39;</span><span style="color: #D4D4D4"> =&gt; &#91;</span><span style="color: #CE9178">&#39;Content-Type&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;application/json&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;body&#39;</span><span style="color: #D4D4D4"> =&gt; &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;email&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;john@example.com&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;is_active&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            &#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">        ]);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$builder</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">verify</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> () {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$client</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">BillingClient</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$customer</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$client</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">getCustomer</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertSame</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;john@example.com&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$customer</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;email&#39;</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertTrue</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$customer</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;is_active&#39;</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">    });</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>This test:</p>



<ul class="wp-block-list">
<li>defines the expected request</li>



<li>defines the expected response</li>



<li>generates a <strong>contract file</strong> (Pact file)</li>
</ul>



<p>This contract is the <strong>single source of truth</strong> between consumer and provider.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-9b5102f6aa14e3aed06d0e0cedc33a57" style="color:#0092eb">Verifying the Contract on the Provider Side</h3>



<p>On the provider side, the API team runs Pact verification:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>pact-verifier --provider-base-url=http://localhost:8000 \
 --pact-url=./pacts/billing-consumer.json
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #DCDCAA">pact-verifier</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">--provider-base-url=http://localhost:8000</span><span style="color: #D4D4D4"> </span><span style="color: #D7BA7D">\</span></span>
<span class="line"><span style="color: #D4D4D4"> </span><span style="color: #569CD6">--pact-url=./pacts/billing-consumer.json</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">Bash</span></div>



<p>If the API no longer returns <code>is_active</code>, verification fails.</p>



<p>This is the key difference from mocks:</p>



<ul class="wp-block-list">
<li>mocks only test the consumer</li>



<li>contract testing tests the <strong>agreement</strong></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-60f6d77b17f2509d3b9848e4ea0f43cd" style="color:#0092eb">Running Contract Tests in CI</h3>



<p>This is where contract testing becomes powerful.</p>



<p>Typical CI flow:</p>



<ol class="wp-block-list">
<li>Consumer tests generate Pact contracts.</li>



<li>Provider pipeline verifies contracts.</li>



<li>Pipeline fails if contracts are broken.</li>
</ol>



<p>This creates a safety net:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>API changes cannot be deployed if they break existing consumers.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-08e8c4152a25791e845da3854e58f9c9" style="color:#0092eb">Common Mistakes with Contract Testing</h2>



<h3 class="wp-block-heading">Over-Specifying Responses</h3>



<p>Don’t lock down every field if you don’t need it.<br>Only define what your app actually uses.</p>



<h3 class="wp-block-heading">Only Testing Happy Paths</h3>



<p>Include error contracts:</p>



<ul class="wp-block-list">
<li>404 responses</li>



<li>validation errors</li>



<li>unauthorized responses</li>
</ul>



<h3 class="wp-block-heading">Not Versioning Contracts</h3>



<p>Treat contracts like code. Version them.</p>



<h3 class="wp-block-heading">Not Enforcing Contracts in CI</h3>



<p>If no pipeline enforces them, contract testing is just documentation.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-cf2e64a689b09ec738026175f62a3250" style="color:#0092eb">When Contract Testing Is Worth It (and When It’s Overkill)</h2>



<p><strong>Contract testing is worth it when:</strong></p>



<ul class="wp-block-list">
<li>multiple teams own different services</li>



<li>APIs evolve frequently</li>



<li>breaking changes are expensive</li>
</ul>



<p><strong>It’s probably overkill when:</strong></p>



<ul class="wp-block-list">
<li>the integration is trivial</li>



<li>you don’t control the provider</li>



<li>the API is stable and rarely changes</li>
</ul>



<p>Contract testing is a scalpel, not a hammer. Use it where API stability matters.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-b77233e4ae2a1827c326fd2a90356849" style="color:#0092eb">How Contract Testing Fits into Your Laravel Testing Strategy</h2>



<p>This is how I think about testing layers in real projects:</p>



<ul class="wp-block-list">
<li><strong>Unit tests</strong> – business logic</li>



<li><strong>Feature tests</strong> – HTTP flows</li>



<li><strong>Mocks</strong> – isolate slow or unstable services</li>



<li><strong>Contract tests</strong> – protect integrations from breaking changes</li>
</ul>



<p>Mocks keep your tests fast.<br>Contract tests keep your integrations honest.</p>



<p>They solve different problems and work best together.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-e6f4ac7d9acd8c30eb3fe27df30760b9" style="color:#0092eb">Final Thoughts</h2>



<p>Mocking external APIs is necessary — but it’s not sufficient.</p>



<p>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.</p>



<p>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.</p>



<p></p>
<p>The post <a href="https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/">Contract Testing External APIs in PHP with Pact (Real Laravel Example)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Builder Pattern in PHP/Laravel: Building Clean and Flexible Order Objects</title>
		<link>https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/</link>
					<comments>https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 07 Feb 2026 12:42:27 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<category><![CDATA[software-design]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3165</guid>

					<description><![CDATA[<p>In practice, orders in e-commerce systems often evolve over time, which makes them a perfect candidate for the Builder Pattern. 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 Order class can quickly become complex. [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/">Builder Pattern in PHP/Laravel: Building Clean and Flexible Order Objects</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p><strong>In practice</strong>, orders in e-commerce systems often evolve over time, which makes them a perfect candidate for the Builder Pattern.</p>



<p>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 <strong>Order class</strong> 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:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$order = new Order(
    $customerId,
    $items,
    $paymentMethod,
    $shippingAddress,
    $discountCode,
    $giftWrap = true,
    $specialInstructions = null
);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$customerId</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$items</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$paymentMethod</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$shippingAddress</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$discountCode</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$giftWrap</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$specialInstructions</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">null</span></span>
<span class="line"><span style="color: #D4D4D4">);</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Not only is it hard to read, but it’s also easy to make mistakes when creating orders. <strong>Because of this</strong>, 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.</p>



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



<p>Previous article in this category: <a href="https://codecraftdiary.com/2026/01/17/factory-method-in-php/">https://codecraftdiary.com/2026/01/17/factory-method-in-php/</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Why the Builder Pattern Works for Orders</h2>



<p>The Builder Pattern is especially useful when:</p>



<ul class="wp-block-list">
<li>A class has <strong>many optional parameters</strong></li>



<li>The class requires <strong>complex setup or validation</strong></li>



<li>You want to <strong>avoid long constructors</strong></li>



<li>You need <strong>clear, readable, fluent code</strong></li>
</ul>



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



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Step 1: Designing the Order Class</h2>



<p>Let’s start with a simple <code>Order</code> class that represents a finalized order object. We’ll assume that once an order is created, it’s immutable:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>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
    ) {}
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$customerId</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$items</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$paymentMethod</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$shippingAddress</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> ?</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$discountCode</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">bool</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$giftWrap</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">false</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> ?</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$specialInstructions</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">null</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Step 2: Creating the Order Builder</h2>



<p>We can create an <strong>OrderBuilder</strong> class to handle object creation step by step:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>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->customerId = $customerId;
        return $this;
    }

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

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

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

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

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

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

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

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

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

        return new Order(
            $this->customerId,
            $this->items,
            $this->paymentMethod,
            $this->shippingAddress,
            $this->discountCode,
            $this->giftWrap,
            $this->specialInstructions
        );
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderBuilder</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$customerId</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$items</span><span style="color: #D4D4D4"> = [];</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$paymentMethod</span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">&#39;credit_card&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$shippingAddress</span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">&#39;&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> ?</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$discountCode</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">bool</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$giftWrap</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">false</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> ?</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$specialInstructions</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">self</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">customerId</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$customerId</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">self</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">customerId</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$customerId</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">addItem</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">self</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">items</span><span style="color: #D4D4D4">[] = </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">paymentMethod</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$method</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">self</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">paymentMethod</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$method</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">shippingAddress</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$address</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">self</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">shippingAddress</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$address</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">discountCode</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$code</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">self</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">discountCode</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$code</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">giftWrap</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">bool</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$flag</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">self</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">giftWrap</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$flag</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">specialInstructions</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$instructions</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">self</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">specialInstructions</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$instructions</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">build</span><span style="color: #D4D4D4">(): </span><span style="color: #4EC9B0">Order</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #DCDCAA">empty</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">customerId</span><span style="color: #D4D4D4">)) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">\InvalidArgumentException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Customer ID is required.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #DCDCAA">empty</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">items</span><span style="color: #D4D4D4">)) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">\InvalidArgumentException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;At least one item is required.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #DCDCAA">empty</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">shippingAddress</span><span style="color: #D4D4D4">)) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">\InvalidArgumentException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Shipping address is required.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">customerId</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">items</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">paymentMethod</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">shippingAddress</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">discountCode</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">giftWrap</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">specialInstructions</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>Notice a few key things:</p>



<ul class="wp-block-list">
<li><strong>Fluent interface:</strong> Each setter returns <code>$this</code>, allowing chained calls.</li>



<li><strong>Validation in build():</strong> Mandatory fields are checked before creating the <code>Order</code>.</li>



<li><strong>Stepwise construction:</strong> You can add items or set options one by one.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Step 3: Using the Builder</h2>



<p>Here’s how creating an order looks <strong>without a builder</strong>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$order = new Order(
    123,
    [&#91;'product_id' => 1, 'quantity' => 2&#93;, &#91;'product_id' => 5, 'quantity' => 1&#93;],
    'paypal',
    '123 Main Street, City, Country',
    'DISCOUNT10',
    true,
    'Leave at the front door'
);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    [&#91;</span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">2</span><span style="color: #D4D4D4">&#93;, &#91;</span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">&#93;],</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;paypal&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;123 Main Street, City, Country&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;DISCOUNT10&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;Leave at the front door&#39;</span></span>
<span class="line"><span style="color: #D4D4D4">);</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p><strong>As a result</strong>, it’s hard to read and error-prone.</p>



<p>With the builder, it becomes:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$order = OrderBuilder::create()
    ->customerId(123)
    ->addItem(&#91;'product_id' => 1, 'quantity' => 2&#93;)
    ->addItem(&#91;'product_id' => 5, 'quantity' => 1&#93;)
    ->paymentMethod('paypal')
    ->shippingAddress('123 Main Street, City, Country')
    ->discountCode('DISCOUNT10')
    ->giftWrap(true)
    ->specialInstructions('Leave at the front door')
    ->build();
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">OrderBuilder</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">customerId</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">addItem</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">2</span><span style="color: #D4D4D4">&#93;)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">addItem</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">&#93;)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">paymentMethod</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;paypal&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">shippingAddress</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;123 Main Street, City, Country&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">discountCode</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;DISCOUNT10&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">giftWrap</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">specialInstructions</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Leave at the front door&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">build</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<ul class="wp-block-list">
<li>Much cleaner and readable.</li>



<li>Easy to skip optional fields or add more items.</li>



<li>Validation ensures that you set all required fields.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Step 4: Integrating with Laravel Jobs or Services</h2>



<p>In Laravel, orders are often <strong>processed asynchronously</strong> using Jobs or Services. Builders make this easier:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$order = OrderBuilder::create()
    ->customerId($user->id)
    ->addItem(&#91;'product_id' => $product->id, 'quantity' => 1&#93;)
    ->shippingAddress($user->address)
    ->build();

dispatch(new ProcessOrderJob($order));
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">OrderBuilder</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">customerId</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">addItem</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">&#93;)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">shippingAddress</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">address</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">build</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ProcessOrderJob</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">));</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>You can also <strong>create reusable templates</strong> for common order setups:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>class OrderBuilderFactory
{
    public static function giftOrder(int $customerId, array $items, string $address): OrderBuilder
    {
        return OrderBuilder::create()
            ->customerId($customerId)
            ->shippingAddress($address)
            ->giftWrap(true)
            ->paymentMethod('credit_card')
            ->addItem(...$items);
    }
}

$order = OrderBuilderFactory::giftOrder($user->id, $items, $user->address)->build();
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderBuilderFactory</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">giftOrder</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$customerId</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$items</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$address</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">OrderBuilder</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderBuilder</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">customerId</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$customerId</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">shippingAddress</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$address</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">giftWrap</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">paymentMethod</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;credit_card&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">addItem</span><span style="color: #D4D4D4">(...</span><span style="color: #9CDCFE">$items</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">OrderBuilderFactory</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">giftOrder</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$items</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">address</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">build</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p>This pattern is perfect when you have <strong>standardized order types</strong>, like gifts or bulk orders.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Step 5: Advantages of Using the Builder Pattern</h2>



<ol class="wp-block-list">
<li><strong>Readability:</strong> The creation of orders reads like a series of instructions.</li>



<li><strong>Validation:</strong> All required fields are checked in one place.</li>



<li><strong>Flexibility:</strong> Optional fields can be skipped without affecting the rest.</li>



<li><strong>Testability:</strong> Builders can be tested independently from the <code>Order</code> class.</li>



<li><strong>Extensibility:</strong> Adding new optional features (e.g., gift wrap, promo codes) doesn’t break existing code.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Step 6: When Not to Use a Builder</h2>



<p>You don’t always need builders. Avoid using them if:</p>



<ul class="wp-block-list">
<li>The class only has <strong>2–3 parameters</strong>.</li>



<li>Objects are <strong>simple DTOs</strong> without optional parameters.</li>



<li>Fluent APIs add unnecessary complexity.</li>
</ul>



<p>For small objects, a simple constructor or <strong>named constructors</strong> may suffice.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Step 7: Summary</h2>



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



<p>For an <code>Order</code> class with multiple optional features, the builder provides a <strong>clear, step-by-step way</strong> to create instances without dealing with confusing long constructors.</p>



<p><strong>Key takeaways:</strong></p>



<ul class="wp-block-list">
<li>Use the builder for <strong>complex or optional-heavy objects</strong>.</li>



<li>Keep <strong>validation in the build() method</strong>.</li>



<li>Leverage <strong>fluent interfaces</strong> for readability.</li>



<li>Consider <strong>factory methods for reusable templates</strong>.</li>
</ul>



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



<p></p>
<p>The post <a href="https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/">Builder Pattern in PHP/Laravel: Building Clean and Flexible Order Objects</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
