<?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 Jul 2026 09:51:36 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.1</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>Testing Laravel Events and Listeners: Ensuring Reliable Asynchronous Workflows</title>
		<link>https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/</link>
					<comments>https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 13 Jul 2026 16:37:00 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3314</guid>

					<description><![CDATA[<p>In modern Laravel applications, events and listeners are the glue that holds our complex business logic together. They allow us to decouple our code, keeping controllers thin and services focused. However, as applications scale, this &#8220;decoupling&#8221; can become a testing nightmare. We’ve all been there. You trigger an OrderPlaced event. It’s supposed to send an [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/">Testing Laravel Events and Listeners: Ensuring Reliable Asynchronous Workflows</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In modern Laravel applications, events and listeners are the glue that holds our complex business logic together. They allow us to decouple our code, keeping controllers thin and services focused. However, as applications scale, this &#8220;decoupling&#8221; can become a testing nightmare.</p>



<p class="wp-block-paragraph">We’ve all been there. You trigger an <code>OrderPlaced</code> event. It’s supposed to send an email, update the inventory, and notify the warehouse. One day, you realize the email never sent because an exception in the inventory listener swallowed the entire process. If you aren’t testing your events properly, you aren’t just missing code coverage—you are leaving your business workflows to chance.</p>



<p class="wp-block-paragraph">In this guide, we’ll move beyond basic <code>Event::fake()</code> assertions and explore how to build a robust testing strategy for event-driven Laravel applications.</p>



<p class="wp-block-paragraph">Previous article in this category: <a href="https://codecraftdiary.com/2026/06/22/tdd-in-laravel/">https://codecraftdiary.com/2026/06/22/tdd-in-laravel/</a></p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-62a8045fe530e357e172ca2aaaf3dd5d">The Problem with &#8220;Testing Too Much&#8221;</h3>



<p class="wp-block-paragraph">When developers start testing events, the default instinct is to reach for <code>Event::fake()</code>. It’s easy, it’s fast, and it makes the test pass.</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_order_is_placed(): void
{
    Event::fake();

    $this->post('/checkout', &#91;...&#93;);

    Event::assertDispatched(OrderPlaced::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: #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_placed</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">Event</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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/checkout&#39;</span><span style="color: #D4D4D4">, &#91;...&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderPlaced</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></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 class="wp-block-paragraph">This test tells us one thing: <em>Did we trigger the event?</em> It tells us absolutely nothing about whether the listeners actually work or if they communicate correctly with each other. If you rely solely on faking, your test suite becomes a &#8220;smoke screen&#8221; that passes even when your underlying infrastructure is broken.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-f88a2eced459e8b9177e6c53aa819221">1. Unit Testing Listeners in Isolation</h3>



<p class="wp-block-paragraph">The best way to ensure reliability is to treat your Listeners like any other service class. A listener should have one job. If it has complex logic, extract it into a dedicated Action or Service class and test <em>that</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>// app/Listeners/SendOrderConfirmation.php
public function handle(OrderPlaced $event): void
{
    // Don't put business logic here!
    $this->mailer->sendConfirmation($event->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: #6A9955">// app/Listeners/SendOrderConfirmation.php</span></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">handle</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderPlaced</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$event</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: #6A9955">// Don&#39;t put business logic here!</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">mailer</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">sendConfirmation</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$event</span><span style="color: #D4D4D4">-&gt;</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></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 class="wp-block-paragraph">By keeping the listener thin, your unit test for <code>SendOrderConfirmation</code> becomes trivial. You can mock the <code>Mailer</code> service and simply verify the <code>handle()</code> method is called correctly. This is fast, deterministic, and catches bugs in the communication layer without overhead.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-337d893f015fcf8a98e2842fb943b4d0">2. Integration Testing: The &#8220;End-to-End&#8221; Event Flow</h3>



<p class="wp-block-paragraph">When you want to verify that the <code>OrderPlaced</code> event actually triggers the <code>SendOrderConfirmation</code> listener and writes the correct data to the database, you need an integration test.</p>



<p class="wp-block-paragraph">Crucially, <strong>do not fake the event here.</strong> Instead, allow the event to dispatch and verify the 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_order_placed_event_triggers_side_effects(): void
{
    // 1. Arrange: Setup user and cart
    $order = Order::factory()->create();

    // 2. Act: Trigger the event directly
    OrderPlaced::dispatch($order);

    // 3. Assert: Check side effects, not just the dispatch
    $this->assertDatabaseHas('emails', &#91;
        'order_id' => $order->id,
        'status' => 'sent'
    &#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_placed_event_triggers_side_effects</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: #6A9955">// 1. Arrange: Setup user and cart</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: #6A9955">// 2. Act: Trigger the event directly</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">OrderPlaced</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: #D4D4D4">    </span><span style="color: #6A9955">// 3. Assert: Check side effects, not just the dispatch</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">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;emails&#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">        </span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;sent&#39;</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>



<p class="wp-block-paragraph">This approach proves that your Service Provider is correctly registered and that the binding between the Event and Listener is active.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-aa24c4d2aafa5dbf2f405346482af374">3. Handling Asynchronous Queues</h3>



<p class="wp-block-paragraph">The biggest trap is the mix of synchronous and asynchronous listeners. If your listener implements <code>ShouldQueue</code>, <code>Event::fake()</code> will prevent the job from ever being pushed to the queue.</p>



<p class="wp-block-paragraph">To test queued listeners, use the <code>Bus</code> and <code>Queue</code> facades in tandem:</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_placed_event_queues_notification(): void
{
    Queue::fake();

    Event::dispatch(new OrderPlaced($order));

    Queue::assertPushed(SendOrderConfirmation::class, function ($job) use ($order) {
        return $job->order->id === $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_order_placed_event_queues_notification</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">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: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><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">OrderPlaced</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: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">SendOrderConfirmation</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: #9CDCFE">id</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 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 class="wp-block-paragraph"><strong>Pro Tip:</strong> If you are testing a complex flow, don&#8217;t forget to test the failure state. Use <code>Queue::assertPushed</code> to verify that the retry logic or failure handling (like <code>failed()</code> methods) is configured for your mission-critical jobs.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-2130a5d73c78be2ed27352de9193bc6c">4. Avoiding &#8220;The Silent Failure&#8221;</h3>



<p class="wp-block-paragraph">One common mistake is neglecting what happens when a listener fails. If your application relies on a chain of events, you must test the &#8220;atomic&#8221; nature of the flow.</p>



<p class="wp-block-paragraph">If you are using Laravel 11/12+ features, ensure you are testing your custom <code>shouldDiscoverEvents</code> logic if you use event discovery. Hidden logic is the enemy of maintainable tests. If a developer adds a new listener to a directory, does your test suite automatically include it? By writing explicit integration tests for critical flows, you ensure that even &#8220;magically&#8221; discovered events are held accountable.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-c4f283b0f6c76a20ae00f9af7d140a64">Testing Checklist for Events</h3>



<p class="wp-block-paragraph">To keep your test suite maintainable as your project grows:</p>



<ul class="wp-block-list">
<li><strong>Logic Extraction:</strong> If a listener is more than 5 lines, move the logic to an Action class.</li>



<li><strong>Fake Sparingly:</strong> Use <code>Event::fake()</code> only when you specifically want to verify the <em>triggering</em> of an event, not its <em>outcome</em>.</li>



<li><strong>Verify Side Effects:</strong> Always write at least one integration test that lets the event fire &#8220;for real&#8221; to ensure the pipeline is wired up correctly.</li>



<li><strong>Queue Awareness:</strong> Always distinguish between testing the dispatch and testing the queued job execution.</li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/">Testing Laravel Events and Listeners: Ensuring Reliable Asynchronous Workflows</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The Specification Pattern in Laravel: Refactoring Complex Business Rules</title>
		<link>https://codecraftdiary.com/2026/07/06/specification-pattern-in-laravel/</link>
					<comments>https://codecraftdiary.com/2026/07/06/specification-pattern-in-laravel/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 13:45:00 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3311</guid>

					<description><![CDATA[<p>As our Laravel applications grow, we inevitably face a common architectural dilemma: where do we put complex business logic that dictates whether an entity meets certain criteria? You start simple. A user is allowed to purchase a premium item if they are active. Easy—you write a quick Eloquent scope. But three months later, the business [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/07/06/specification-pattern-in-laravel/">The Specification Pattern in Laravel: Refactoring Complex Business Rules</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">As our Laravel applications grow, we inevitably face a common architectural dilemma: <strong>where do we put complex business logic that dictates whether an entity meets certain criteria?</strong></p>



<p class="wp-block-paragraph">You start simple. A user is allowed to purchase a premium item if they are active. Easy—you write a quick Eloquent scope. But three months later, the business team changes the rules. Now, a user can only buy that item if they are active, have a verified email, have spent at least $500 in the last 90 days, and are <em>not</em> flagged for suspicious activity.</p>



<p class="wp-block-paragraph">Suddenly, your code becomes littered with long chains of Eloquent scopes in your queries, and worse, identical blocks of <code class="">if-else</code> statements in your PHP services to check the exact same logic for already-loaded models.</p>



<p class="wp-block-paragraph">This is where the codebase starts to smell. We are violating the Single Responsibility Principle, duplicating rules, and creating a maintenance nightmare. To fix this, developers often reach for complex design patterns that end up overengineering the solution, violating the KISS (Keep It Simple, Stupid) principle.</p>



<p class="wp-block-paragraph">But there is a elegant, highly practical pattern that solves this perfectly without breaking a sweat: <strong>The Specification Pattern</strong>.</p>



<p class="wp-block-paragraph"><strong>Previous article in refactoring category:</strong> <a href="https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/</a></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-52b213172ecff073f11c8f858cab5802">The Real-World Problem: The &#8220;Premium Customer&#8221; Mess</h2>



<p class="wp-block-paragraph">Let’s look at a concrete example. Imagine an e-commerce platform where we need to determine if a customer qualifies for a VIP Discount.</p>



<p class="wp-block-paragraph">The business rules for a VIP customer are:</p>



<ol start="1" class="wp-block-list">
<li>The account must be active.</li>



<li>The user must have a verified email.</li>



<li>They must have made at least 5 orders in total.</li>



<li>They must not be on our fraud blacklist.</li>
</ol>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-7f551198e4156a745826ff4c63bc2b58">The Naive Laravel Approach</h3>



<p class="wp-block-paragraph">Typically, you see developers solving this in two ways. First, they write a massive query in a controller or 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>// In a Controller or Service
$vipUsers = User::query()
    ->where('is_active', true)
    ->whereNotNull('email_verified_at')
    ->whereHas('orders', function ($query) {
        $query->where('status', 'completed');
    }, '>=', 5)
    ->whereDoesntHave('flags', function ($query) {
        $query->where('type', 'fraud');
    })
    ->get();
</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">// In a Controller or Service</span></span>
<span class="line"><span style="color: #9CDCFE">$vipUsers</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">query</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;is_active&#39;</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">whereNotNull</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;email_verified_at&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">whereHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;completed&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }, </span><span style="color: #CE9178">&#39;&gt;=&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">whereDoesntHave</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;flags&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;type&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;fraud&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    })</span></span>
<span class="line"><span style="color: #D4D4D4">    -&gt;</span><span style="color: #DCDCAA">get</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 class="wp-block-paragraph">This works fine for fetching data from the database. But what happens when you already have a <code class="">User</code> instance in memory—say, inside a Job or an Event Listener—and you need to check if <em>this specific user</em> qualifies?</p>



<p class="wp-block-paragraph">You end up duplicating the logic in pure PHP:</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 qualifiesForVipDiscount(User $user): bool
{
    return $user->is_active 
        &amp;&amp; $user->email_verified_at !== null
        &amp;&amp; $user->orders()->where('status', 'completed')->count() >= 5
        &amp;&amp; !$user->flags()->where('type', 'fraud')->exists();
}
</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">qualifiesForVipDiscount</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">bool</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: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">is_active</span><span style="color: #D4D4D4"> </span></span>
<span class="line"><span style="color: #D4D4D4">        &amp;&amp; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">email_verified_at</span><span style="color: #D4D4D4"> !== </span><span style="color: #569CD6">null</span></span>
<span class="line"><span style="color: #D4D4D4">        &amp;&amp; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">orders</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;completed&#39;</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">count</span><span style="color: #D4D4D4">() &gt;= </span><span style="color: #B5CEA8">5</span></span>
<span class="line"><span style="color: #D4D4D4">        &amp;&amp; !</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">flags</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;type&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;fraud&#39;</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">exists</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-vivid-cyan-blue-color has-text-color has-link-color wp-elements-a89d0507d8998c98779e0aba50a651ac">Why this breaks the KISS principle:</h3>



<ul class="wp-block-list">
<li><strong>Duplication:</strong> If the fraud rule changes, you have to find and rewrite both the SQL/Eloquent logic and the in-memory PHP logic.</li>



<li><strong>Model Bloat:</strong> Shoving this into the <code class="">User</code> model makes it a &#8220;God Object&#8221; over time.</li>



<li><strong>Hidden Rules:</strong> Your business logic is trapped inside technical database implementation details.</li>
</ul>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-1b1d476c8f621b218a3bc17b81d3d876">Enter the Specification Pattern</h2>



<p class="wp-block-paragraph">The Specification Pattern solves this by turning a business rule into a first-class citizen—a single, isolated, reusable class.</p>



<p class="wp-block-paragraph">A strict specification pattern can sometimes get overly complex with abstract syntax trees and custom expression builders. To keep it <strong>KISS-compliant</strong>, we are going to build a pragmatic version tailored for modern PHP 8.5+ and Laravel.</p>



<p class="wp-block-paragraph">Let&#8217;s define a simple interface for our specifications:</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>namespace App\Specifications;

use Illuminate\Database\Eloquent\Builder;

interface Specification
{
    /**
     * Check if a given object satisfies the specification in-memory.
     */
    public function isSatisfiedBy(mixed $candidate): bool;

    /**
     * Apply the specification directly to an Eloquent query builder.
     */
    public function apply(Builder $query): void;
}
</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\Specifications</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Database\Eloquent\</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">interface</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Specification</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">/**</span></span>
<span class="line"><span style="color: #6A9955">     * Check if a given object satisfies the specification in-memory.</span></span>
<span class="line"><span style="color: #6A9955">     */</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">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">mixed</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">bool</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">/**</span></span>
<span class="line"><span style="color: #6A9955">     * Apply the specification directly to an Eloquent query builder.</span></span>
<span class="line"><span style="color: #6A9955">     */</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">apply</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</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>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-70ffc624b05e5f91ba9b50f8ca1d5665">Implementing the Specification</h2>



<p class="wp-block-paragraph">Let&#8217;s build our <code>IsVipCustomer</code> specification. Instead of writing abstract logic, we put our exact four business rules inside this class.</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>namespace App\Specifications;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;

class IsVipCustomer implements Specification
{
    public function isSatisfiedBy(mixed $candidate): bool
    {
        if (!$candidate instanceof User) {
            return false;
        }

        return $candidate->is_active
            &amp;&amp; $candidate->email_verified_at !== null
            &amp;&amp; $candidate->orders->where('status', 'completed')->count() >= 5
            &amp;&amp; !$candidate->flags->contains('type', 'fraud');
    }

    public function apply(Builder $query): void
    {
        $query->where('is_active', true)
            ->whereNotNull('email_verified_at')
            ->whereHas('orders', function ($q) {
                $q->where('status', 'completed');
            }, '>=', 5)
            ->whereDoesntHave('flags', function ($q) {
                $q->where('type', 'fraud');
            });
    }
}
</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\Specifications</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Models\</span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Database\Eloquent\</span><span style="color: #4EC9B0">Builder</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">IsVipCustomer</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Specification</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">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">mixed</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">bool</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: #9CDCFE">$candidate</span><span style="color: #D4D4D4"> instanceof </span><span style="color: #4EC9B0">User</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">false</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: #9CDCFE">$candidate</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">is_active</span></span>
<span class="line"><span style="color: #D4D4D4">            &amp;&amp; </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">email_verified_at</span><span style="color: #D4D4D4"> !== </span><span style="color: #569CD6">null</span></span>
<span class="line"><span style="color: #D4D4D4">            &amp;&amp; </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">orders</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;completed&#39;</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">count</span><span style="color: #D4D4D4">() &gt;= </span><span style="color: #B5CEA8">5</span></span>
<span class="line"><span style="color: #D4D4D4">            &amp;&amp; !</span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">flags</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">contains</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;type&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;fraud&#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: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">apply</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$query</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: #9CDCFE">$query</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;is_active&#39;</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">whereNotNull</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;email_verified_at&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">whereHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;completed&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">            }, </span><span style="color: #CE9178">&#39;&gt;=&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">whereDoesntHave</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;flags&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;type&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;fraud&#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>
<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 class="wp-block-paragraph">Notice how clean this is. The technical details of <em>how</em> we determine a VIP customer are encapsulated in exactly one file.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-8b775c425b7a85645da1d846805f5f14">Making It Truly Useful: Combining Specifications</h2>



<p class="wp-block-paragraph">Where this pattern completely demolishes standard refactoring approaches is when business requirements change or combine. What if we want to find users who are <strong>VIP Customers</strong> AND are <strong>Located in the EU</strong> (for tax or shipping promotions)?</p>



<p class="wp-block-paragraph">Instead of writing a third massive scope, we can create a simple composition layer. Let&#8217;s create an <code>AndSpecification</code>:</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>namespace App\Specifications;

use Illuminate\Database\Eloquent\Builder;

class AndSpecification implements Specification
{
    private array $specifications;

    public function __construct(Specification ...$specifications)
    {
        $this->specifications = $specifications;
    }

    public function isSatisfiedBy(mixed $candidate): bool
    {
        foreach ($this->specifications as $spec) {
            if (!$spec->isSatisfiedBy($candidate)) {
                return false;
            }
        }
        return true;
    }

    public function apply(Builder $query): void
    {
        foreach ($this->specifications as $spec) {
            $spec->apply($query);
        }
    }
}
</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\Specifications</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Database\Eloquent\</span><span style="color: #4EC9B0">Builder</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">AndSpecification</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Specification</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">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$specifications</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">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Specification</span><span style="color: #D4D4D4"> ...</span><span style="color: #9CDCFE">$specifications</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">specifications</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$specifications</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">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">mixed</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">bool</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">specifications</span><span style="color: #D4D4D4"> as </span><span style="color: #9CDCFE">$spec</span><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: #9CDCFE">$spec</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$candidate</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">false</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 style="color: #D4D4D4">        </span><span style="color: #C586C0">return</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>
<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">apply</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$query</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">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">specifications</span><span style="color: #D4D4D4"> as </span><span style="color: #9CDCFE">$spec</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$spec</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">apply</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$query</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 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>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-ef9b222c0395fb44f14fbddbfa5dc219">Putting It Into Practice (Where It Pays Off)</h2>



<p class="wp-block-paragraph">Let&#8217;s look at how this simplifies our daily Laravel tasks. We&#8217;ll look at a Controller (database querying) and a Service/Job (in-memory validation).</p>



<h3 class="wp-block-heading">Scenario A: Querying the Database</h3>



<p class="wp-block-paragraph">Inside your Admin Dashboard controller, you need to list all VIP customers from the EU to send them a special newsletter.</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>namespace App\Http\Controllers;

use App\Models\User;
use App\Specifications\AndSpecification;
use App\Specifications\IsVipCustomer;
use App\Specifications\IsLocatedInEU;

class VipNewsletterController extends Controller
{
    public function __invoke()
    {
        // Define the composite rule
        $vipInEuSpec = new AndSpecification(
            new IsVipCustomer(),
            new IsLocatedInEU()
        );

        // Apply it directly to the query
        $query = User::query();
        $vipInEuSpec->apply($query);
        
        $users = $query->paginate(30);

        return view('admin.vip-newsletter', compact('users'));
    }
}
</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\Http\Controllers</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Models\</span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Specifications\</span><span style="color: #4EC9B0">AndSpecification</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Specifications\</span><span style="color: #4EC9B0">IsVipCustomer</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Specifications\</span><span style="color: #4EC9B0">IsLocatedInEU</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">VipNewsletterController</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">__invoke</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">// Define the composite rule</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$vipInEuSpec</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">AndSpecification</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">IsVipCustomer</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">IsLocatedInEU</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">// Apply it directly to the query</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">query</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$vipInEuSpec</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">apply</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$query</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">$users</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">paginate</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">30</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">view</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;admin.vip-newsletter&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #DCDCAA">compact</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;users&#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>
<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">Scenario B: In-Memory Domain Validation</h3>



<p class="wp-block-paragraph">Now imagine a completely different part of the app. A user clicks &#8220;Claim VIP Reward&#8221;. The user is already logged in, so we already have the <code>$user</code> instance loaded in memory. We don&#8217;t want to hit the database with complex heavy joins again if we can avoid 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>namespace App\Http\Controllers;

use App\Specifications\IsVipCustomer;
use Illuminate\Http\Request;

class RewardController extends Controller
{
    public function claim(Request $request, IsVipCustomer $vipSpec)
    {
        $user = $request->user();

        // Check the exact same business rule in-memory
        if (!$vipSpec->isSatisfiedBy($user)) {
            return response()->json(&#91;
                'error' => 'You do not qualify for this reward.'
            &#93;, 403);
        }

        // Process reward...
        return response()->json(&#91;'success' => 'Reward claimed!'&#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\Http\Controllers</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Specifications\</span><span style="color: #4EC9B0">IsVipCustomer</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Http\</span><span style="color: #4EC9B0">Request</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">RewardController</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">claim</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">IsVipCustomer</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$vipSpec</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">user</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Check the exact same business rule in-memory</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (!</span><span style="color: #9CDCFE">$vipSpec</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</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: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;error&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;You do not qualify for this reward.&#39;</span></span>
<span class="line"><span style="color: #D4D4D4">            &#93;, </span><span style="color: #B5CEA8">403</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">// Process reward...</span></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">(&#91;</span><span style="color: #CE9178">&#39;success&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Reward claimed!&#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>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-0d93d76c343db7faa608b3f985122f00">Why This is NOT Overengineering</h2>



<p class="wp-block-paragraph">If you look at the code above, a skeptic might say: <em>&#8220;Why did we create three classes when a couple of Eloquent scopes could do the trick?&#8221;</em></p>



<p class="wp-block-paragraph">Here is why this approach honors the KISS principle when dealing with real complexity:</p>



<ol start="1" class="wp-block-list">
<li><strong>The &#8220;Single Source of Truth&#8221; rule:</strong> If the marketing team changes the definition of a VIP customer to require 10 orders instead of 5, you change <strong>one number in one class</strong>. Your controllers, jobs, and models remain completely untouched.</li>



<li><strong>Database vs. Memory consistency:</strong> Standard Eloquent scopes cannot be run on a regular PHP collection or an already-loaded model without triggering fresh SQL queries. The Specification Pattern bridges this gap natively.</li>



<li><strong>Impeccable Testability:</strong> Testing complex business logic mixed with controllers is painful. Testing a Specification class is a pure unit test. You pass a mock user, assert <code>true</code> or <code>false</code>, and you&#8217;re done in milliseconds.</li>
</ol>



<p class="wp-block-paragraph"></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_user_with_insufficient_orders_is_not_vip()
{
    $user = new User(&#91;'is_active' => true, 'email_verified_at' => now()&#93;);
    $user->setRelation('orders', collect()); // 0 orders
    $user->setRelation('flags', collect());

    $spec = new IsVipCustomer();

    $this->assertFalse($spec->isSatisfiedBy($user));
}
</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_user_with_insufficient_orders_is_not_vip</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">(&#91;</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 style="color: #CE9178">&#39;email_verified_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()&#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">setRelation</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #DCDCAA">collect</span><span style="color: #D4D4D4">()); </span><span style="color: #6A9955">// 0 orders</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">setRelation</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;flags&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #DCDCAA">collect</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$spec</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">IsVipCustomer</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">assertFalse</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$spec</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</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>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-dda3f6f0a64aedcb4c69d3aaa86623f0">Summary: When to Use It</h2>



<p class="wp-block-paragraph">Don&#8217;t use this pattern for simple things like <code>User::where('role', 'admin')</code>. That <em>would</em> be overengineering.</p>



<p class="wp-block-paragraph">Reach for the Specification Pattern when:</p>



<ul class="wp-block-list">
<li>A business rule is dynamic and changes often based on business requirements.</li>



<li>You need to use the exact same rule for both <strong>database filtering</strong> and <strong>in-memory validation</strong>.</li>



<li>You need to combine multiple business rules dynamically depending on the context (e.g., matching different combinations of criteria for user segmentation).</li>
</ul>



<p class="wp-block-paragraph">By encapsulating volatile business logic into isolated specifications, you keep your models thin, your controllers clean, and your application incredibly adaptive to future changes. It’s Clean Architecture at its most practical level.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/07/06/specification-pattern-in-laravel/">The Specification Pattern in Laravel: Refactoring Complex Business Rules</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/07/06/specification-pattern-in-laravel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Automated Testing Strategies for Trunk-Based Development: How to Build a 10-Minute Pipeline</title>
		<link>https://codecraftdiary.com/2026/06/28/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline/</link>
					<comments>https://codecraftdiary.com/2026/06/28/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 28 Jun 2026 13:33:41 +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=3305</guid>

					<description><![CDATA[<p>If you’ve been following my recent posts, you already know I am a big fan of Trunk-Based Development (TBD), tiny pull requests, and deploying to production multiple times a day using feature flags. It sounds like an engineering dream: no long-lived branches, no merge hell on Friday afternoons, and features reaching users in hours instead [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/06/28/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline/">Automated Testing Strategies for Trunk-Based Development: How to Build a 10-Minute Pipeline</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If you’ve been following my recent posts, you already know I am a big fan of Trunk-Based Development (TBD), tiny pull requests, and deploying to production multiple times a day using feature flags. It sounds like an engineering dream: no long-lived branches, no merge hell on Friday afternoons, and features reaching users in hours instead of months.</p>



<p class="wp-block-paragraph">But let’s be honest. When you tell a traditional QA manager or a risk-averse lead that developers are going to merge straight into the main branch and deploy to production ten times a day, they usually have a mini panic attack.</p>



<p class="wp-block-paragraph">And they aren’t entirely wrong to worry.</p>



<p class="wp-block-paragraph">If your team is merging code directly into the trunk every few hours, you cannot rely on manual regression testing. You cannot wait for a weekly QA cycle, and you certainly cannot spend two hours manually clicking through a staging environment to make sure a bugfix didn’t break the payment gateway.</p>



<p class="wp-block-paragraph">To make Trunk-Based Development work without crashing production, you need an automated testing strategy built for speed and high confidence. You need a pipeline that gives you a definitive &#8220;Yes&#8221; or &#8220;No&#8221; in under 10 minutes.</p>



<p class="wp-block-paragraph">Here is how we can build it from a backend perspective.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-5060594ff8dd02b7d58a7b5dd3b59f43">Why the Traditional Testing Pyramid Fails in TBD</h2>



<p class="wp-block-paragraph">We’ve all seen the classic testing pyramid: a massive base of unit test suites, a middle layer of integration tests, and a tiny peak of End-to-End (E2E) UI tests.</p>



<pre class="wp-block-code"><code>       /\
      /  \    &lt;-- E2E / UI Tests (Slow, Flaky)
     /____\
    /      \  &lt;-- Integration Tests (Medium Speed)
   /________\
  /          \ &lt;-- Unit Tests (Fast, Isolated)
 /____________\
</code></pre>



<p class="wp-block-paragraph">In theory, it&#8217;s a great model. In a Trunk-Based Development workflow, however, teams often warp this pyramid into something that looks more like an ice cream cone. Frontend teams or QA engineers might want to cover every single user flow with massive E2E integration suites to feel safe.</p>



<p class="wp-block-paragraph">But as backend developers, we hit a major bottleneck here: <strong>heavy end-to-end tests are notoriously slow.</strong></p>



<p class="wp-block-paragraph">If your CI pipeline takes 45 minutes to run because it needs to spin up frontend clients, log in dummy users, and click through a shopping cart just to verify a backend API change, your TBD flow breaks down entirely. Developers will stop merging frequently. They will accumulate code locally because waiting for the pipeline feels like a punishment. Suddenly, your small PRs become massive again, and you&#8217;re right back to square one.</p>



<p class="wp-block-paragraph">To make TBD work, your pipeline must act as a fast feedback loop, not a bureaucratic checkpoint.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-02dc1bd357e7b225373cfd34b7601281">The Core Strategy: The 10-Minute Rule</h2>



<p class="wp-block-paragraph">The golden rule of Continuous Integration in a fast-paced team is simple: <strong>The commit-to-feedback loop must take less than 10 minutes.</strong></p>



<p class="wp-block-paragraph">If a developer pushes a change to the trunk (or opens a tiny, short-lived PR that will be merged within an hour), they should know almost immediately if they broke the build. If it takes longer than 10 minutes, the developer context-switches, starts a new task, and by the time the build fails, they have to drop what they are doing to fix a problem they’ve already forgotten about.</p>



<p class="wp-block-paragraph">To achieve a 10-minute pipeline, you have to split your testing strategy into two distinct phases: <strong>Pre-Merge (Gatekeeping)</strong> and <strong>Post-Merge (Asynchronous Verification).</strong></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-f1f57c2f981d347f283a7049048294d9">Phase 1: Pre-Merge Validation (The Gatekeeper)</h2>



<p class="wp-block-paragraph">Before code touches the main branch, your CI pipeline needs to run a highly optimized, strictly isolated suite of checks. This is your first line of defense.</p>



<h3 class="wp-block-heading">1. Advanced Unit Testing &amp; Mocking</h3>



<p class="wp-block-paragraph">Unit tests must form the absolute majority of your test suite. They should test pure logic, edge cases, and data transformations without touching external networks, file systems, or databases.</p>



<p class="wp-block-paragraph">Instead of hitting a real database, use clean interfaces and mocks. For example, if you are testing a backend service that calculates a user’s premium subscription discount, do not query the database to fetch the user profile. Pass a mock object or a stub.</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>// Example of a fast, isolated unit test
describe('DiscountService', () => {
  it('should apply a 20% discount for premium users', () => {
    const mockUser = { id: 1, type: 'PREMIUM' };
    const price = 100;
    
    const finalPrice = DiscountService.calculate(mockUser, price);
    
    expect(finalPrice).toBe(80); // Runs in milliseconds
  });
});
</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">// Example of a fast, isolated unit test</span></span>
<span class="line"><span style="color: #DCDCAA">describe</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;DiscountService&#39;</span><span style="color: #D4D4D4">, () </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #DCDCAA">it</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;should apply a 20% discount for premium users&#39;</span><span style="color: #D4D4D4">, () </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">const</span><span style="color: #D4D4D4"> </span><span style="color: #4FC1FF">mockUser</span><span style="color: #D4D4D4"> = { </span><span style="color: #9CDCFE">id:</span><span style="color: #D4D4D4"> </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">type:</span><span style="color: #D4D4D4"> </span><span style="color: #CE9178">&#39;PREMIUM&#39;</span><span style="color: #D4D4D4"> };</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">const</span><span style="color: #D4D4D4"> </span><span style="color: #4FC1FF">price</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">100</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">const</span><span style="color: #D4D4D4"> </span><span style="color: #4FC1FF">finalPrice</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">DiscountService</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">calculate</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">mockUser</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">price</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: #DCDCAA">expect</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">finalPrice</span><span style="color: #D4D4D4">).</span><span style="color: #DCDCAA">toBe</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">80</span><span style="color: #D4D4D4">); </span><span style="color: #6A9955">// Runs in milliseconds</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">JavaScript</span></div>



<p class="wp-block-paragraph">A suite of 2,000 unit tests like this should run in under 60 seconds. If it takes longer, you are likely doing hidden I/O operations.</p>



<h3 class="wp-block-heading">2. Contract Testing over Heavy Integration</h3>



<p class="wp-block-paragraph">When working with backend microservices, a common fear is that changing an API endpoint in Service A will break Service B. The instinctual response is to run massive integration tests that boot up multiple services at once.</p>



<p class="wp-block-paragraph">Don&#8217;t do this in your pre-merge pipeline. Use <strong>Contract Testing</strong> instead (using tools like Pact).</p>



<p class="wp-block-paragraph">Contract testing allows Service B (the consumer) to define a JSON file stating exactly what format it expects from Service A&#8217;s API. During Service A’s pipeline, it simply validates its code against this static contract file. It takes seconds, yet gives you the exact same confidence as a full integration test without spinning up a single extra server or relying on frontend E2E setups.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-e75db8626f67e240ac0ec7c63133e2d8">Phase 2: Post-Merge Validation (The Safety Net)</h2>



<p class="wp-block-paragraph">Once the code passes the 10-minute gatekeeper, it is merged into the trunk. Now, you can trigger the heavier, slower tests asynchronously. This runs in parallel or immediately after deployment to a staging environment.</p>



<h3 class="wp-block-heading">1. Ephemeral Integration Testing</h3>



<p class="wp-block-paragraph">You <em>do</em> need to test real database interactions, but you should do it smart. Use short-lived, isolated environments. Tools like Docker and Testcontainers allow your pipeline to spin up a real PostgreSQL or Redis instance in a container, run database-dependent tests, and tear it down immediately.</p>



<p class="wp-block-paragraph">Keep these tests focused purely on persistence logic—like verifying complex SQL queries or transaction rollbacks—rather than business logic.</p>



<h3 class="wp-block-heading">2. Handling the &#8220;Flaky Test&#8221; Epidemic</h3>



<p class="wp-block-paragraph">Nothing destroys trust in an automated testing pipeline faster than a flaky test—a test that passes 9 times but fails on the 10th for no apparent reason (usually due to network timing, race conditions, or shared state in integration environments).</p>



<p class="wp-block-paragraph">If a developer sees a pipeline fail, checks it, realizes it&#8217;s just &#8220;that one annoying flaky test again,&#8221; and hits <em>Re-run</em>, your testing culture is dying. Developers will start ignoring real failures.</p>



<p class="wp-block-paragraph">You must enforce a strict <strong>Zero Tolerance Policy for Flakiness</strong>:</p>



<ul class="wp-block-list">
<li><strong>Quarantine immediately:</strong> The moment a test shows flaky behavior, move it out of the main pipeline into a separate, non-blocking quarantine suite.</li>



<li><strong>Fix or delete:</strong> If a quarantined test isn&#8217;t fixed within a sprint, delete it. A non-existent test is better than a test that lies to your team.</li>
</ul>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-f6ec4405e6d4d4bd15c15fed0a684126">Continuous Verification: Testing in Production</h2>



<p class="wp-block-paragraph">Even with a perfect pipeline, things will eventually go wrong. The ultimate evolution of a Trunk-Based Development testing strategy is recognizing that <strong>testing doesn&#8217;t stop when you deploy.</strong></p>



<p class="wp-block-paragraph">Since we use Feature Flags (as discussed in my last post), we can merge code to production completely hidden from users. This unlocks a powerful technique: <strong>Canary Deployments.</strong></p>



<p class="wp-block-paragraph">Instead of releasing a change to 100% of your traffic, route just 1% of real users to the new code. Your automated testing strategy here shifts from running test code to monitoring business metrics.</p>



<pre class="wp-block-code"><code>&#91;Trunk Merge] 
     │
     ▼
&#91;10-Min CI Pipeline Passes] 
     │
     ▼
&#91;Deploy Hidden Behind Feature Flag] 
     │
     ▼
&#91;Route 1% of Traffic (Canary)] ───► Monitor: HTTP 500s, Latency
     │
     ├─── Error Spikes? ───► Automatic Rollback (Flag Off)
     │
     └─── Stable? ─────────► Gradual Rollout to 100%
</code></pre>



<p class="wp-block-paragraph">If your APM tool (like Datadog or New Relic) detects a sudden spike in HTTP 500 errors or a drop in checkout conversions for that 1% segment, the feature flag automatically flips off.</p>



<p class="wp-block-paragraph">Production monitoring is the final, most reliable tier of your testing pyramid.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-5e32deab3b45bfd833fd65110e5c79fd">Conclusion: Speed Breeds Quality</h2>



<p class="wp-block-paragraph">Moving to Trunk-Based Development requires a massive shift in how you think about quality assurance. It requires moving away from heavy, slow, protective gates toward fast, automated, and resilient feedback loops.</p>



<p class="wp-block-paragraph">If you focus on keeping your unit tests pure, replacing heavy integration tests with contract testing, ruthlessly quarantining flaky tests, and using production monitoring as your final safety net, you can easily hit that 10-minute pipeline goal.</p>



<p class="wp-block-paragraph">When your pipeline is that fast, developers catch bugs while the code is still fresh in their minds. It makes development predictable, safe, and incredibly fast.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/06/28/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline/">Automated Testing Strategies for Trunk-Based Development: How to Build a 10-Minute Pipeline</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/06/28/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Test-Driven Development in Laravel: The Red-Green-Refactor Loop</title>
		<link>https://codecraftdiary.com/2026/06/22/tdd-in-laravel/</link>
					<comments>https://codecraftdiary.com/2026/06/22/tdd-in-laravel/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 22 Jun 2026 14:31:52 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3300</guid>

					<description><![CDATA[<p>We’ve all been there. You have a new feature to build. You open your IDE, create a Controller, start wiring up the logic, maybe add some validation, and then, hours later, you finally write a test to make sure it actually works. Or, if we’re being honest, maybe you don&#8217;t write the test at all. [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/06/22/tdd-in-laravel/">Test-Driven Development in Laravel: The Red-Green-Refactor Loop</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">We’ve all been there. You have a new feature to build. You open your IDE, create a Controller, start wiring up the logic, maybe add some validation, and then, hours later, you finally write a test to make sure it actually works. Or, if we’re being honest, maybe you don&#8217;t write the test at all.</p>



<p class="wp-block-paragraph">This is the &#8220;code first, ask questions later&#8221; approach. And while it feels fast at the start, it’s exactly how we end up with legacy code that nobody dares to touch. It’s how &#8220;it works on my machine&#8221; turns into a 3:00 AM emergency production fix.</p>



<p class="wp-block-paragraph">Test-Driven Development (TDD) flips this on its head. Instead of writing code to solve a problem and then writing tests to verify it, you write the test <em>first</em>. It sounds counterintuitive, but it’s a game-changer for maintainability. Let’s break it down into the cycle that actually defines professional software development: <strong>Red, Green, Refactor.</strong></p>



<p class="wp-block-paragraph">Previous article in this category: <a href="https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/</a></p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-27715a87d195e3bbbbcc5e6a4a42ba8c">The Cycle Explained: It&#8217;s a Rhythm, Not a Rule</h3>



<p class="wp-block-paragraph">The TDD process is a rhythmic loop that keeps you focused.</p>



<ol start="1" class="wp-block-list">
<li><strong>Red:</strong> You write a test for a tiny, specific piece of functionality that doesn’t exist yet. You run it. It should fail. If it passes immediately, your test is either broken or testing nothing.</li>



<li><strong>Green:</strong> You write the absolute minimum amount of code to make that test pass. Not perfect code, not optimized code—just enough code.</li>



<li><strong>Refactor:</strong> Now that you have a safety net, you clean up. You improve naming, extract methods, remove duplication, and optimize. Because you have the test, you can change the internals without breaking the contract.</li>
</ol>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-4570fcc80fbd53d436e00afcbd445ee5">The &#8220;Why&#8221;: Beyond Just Catching Bugs</h3>



<p class="wp-block-paragraph">Why go through the trouble? TDD is not just about catching bugs. It’s about <strong>design</strong>.</p>



<p class="wp-block-paragraph">When you write the test first, you are forced to step into the shoes of the &#8220;consumer&#8221; of your code. You stop thinking about how a database table is structured and start thinking about how a controller or a service <em>needs</em> to interact with your business logic. If you find it hard to write a test, you’ve just received the most valuable feedback possible: your design is too complex or your coupling is too tight.</p>



<p class="wp-block-paragraph">TDD acts as an early warning system for bad architecture.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-da63e23552d4ca36782b5105c18f324d">A Real-World Example: Building a &#8220;Subscription System&#8221;</h3>



<p class="wp-block-paragraph">Let’s say we are building a subscription feature. We need to ensure that when a user subscribes, they receive a welcome email, and their status is updated in the database.</p>



<h4 class="wp-block-heading">Phase 1: The Red Phase (The Requirement)</h4>



<p class="wp-block-paragraph">We start with a feature test. We are defining the <em>behavior</em>, not the implementation.</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_user_can_subscribe_to_a_plan()
{
    $user = User::factory()->create();
    
    $response = $this->actingAs($user)->post('/subscribe', &#91;
        'plan_id' => 'premium_monthly'
    &#93;);

    $response->assertStatus(200);
    $this->assertDatabaseHas('subscriptions', &#91;
        'user_id' => $user->id,
        'plan' => 'premium_monthly'
    &#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_user_can_subscribe_to_a_plan</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</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 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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">actingAs</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/subscribe&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;plan_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;premium_monthly&#39;</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">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertStatus</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">200</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">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;subscriptions&#39;</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: #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">        </span><span style="color: #CE9178">&#39;plan&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;premium_monthly&#39;</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>



<p class="wp-block-paragraph">Running this results in a 404 or an error, because the route doesn&#8217;t exist. <strong>Red.</strong></p>



<h4 class="wp-block-heading">Phase 2: The Green Phase (The Implementation)</h4>



<p class="wp-block-paragraph">Now, we write just enough code to make this pass. We might put all the logic in the controller (yes, even if it&#8217;s dirty, because that&#8217;s the &#8220;Green&#8221; phase).</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 store(Request $request)
{
    Subscription::create(&#91;
        'user_id' => auth()->id(),
        'plan' => $request->plan_id
    &#93;);
    return response()->json(&#91;'status' => 'success'&#93;, 200);
}
</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">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: #4EC9B0">Subscription</span><span style="color: #D4D4D4">::</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;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">auth</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">id</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;plan&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">plan_id</span></span>
<span class="line"><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: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;success&#39;</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #B5CEA8">200</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 class="wp-block-paragraph">Test passes. <strong>Green.</strong></p>



<h4 class="wp-block-heading">Phase 3: The Refactor Phase (The Cleanup)</h4>



<p class="wp-block-paragraph">Now that the test is green, we look at the controller. It’s getting a bit crowded. We decide to move the logic into a <code>SubscriptionService</code>. We move the logic, run the test again—it still passes! We have total confidence that the refactoring didn&#8217;t break the core business requirement.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-06a511e82ff609cad21cb3e2ca4ad6b3">Deep Dive: Common Pitfalls</h3>



<h4 class="wp-block-heading">1. Over-Mocking</h4>



<p class="wp-block-paragraph">Developers often mock every single dependency, resulting in tests that pass even when the real system is broken. If you are mocking your entire application to test one controller, you aren&#8217;t testing reality. Use real database connections (via <code>RefreshDatabase</code>) and real events whenever possible.</p>



<h4 class="wp-block-heading">2. Testing Implementation vs. Behavior</h4>



<p class="wp-block-paragraph">Don&#8217;t write tests that check if a private method was called or if a specific variable was set. Test the <strong>result</strong>. Did the user get the email? Did the database change? Does the UI show the right message? If you test the implementation, your tests will break every time you refactor, which defeats the whole purpose.</p>



<h4 class="wp-block-heading">3. Avoiding the &#8220;Green&#8221; Trap</h4>



<p class="wp-block-paragraph">Sometimes, you write a test that is too big. You try to test the entire checkout flow in one test. When it fails, you don&#8217;t know why. Break it down. Test the validation of the payment, then the user status, then the email trigger. Small, focused tests are the secret to a high-speed suite.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-421b2580b309e95c5e23bb6f53019027">The &#8220;Slow Down to Speed Up&#8221; Philosophy</h3>



<p class="wp-block-paragraph">The biggest pushback I hear is: <em>&#8220;TDD takes twice as long.&#8221;</em></p>



<p class="wp-block-paragraph">It feels like that at first. You are typing more code. You are thinking more. But think about the time you spend on:</p>



<ul class="wp-block-list">
<li>Manual debugging sessions in the browser.</li>



<li>Writing &#8220;temporary&#8221; logging statements to trace data flow.</li>



<li>Fear of deploying because you don&#8217;t know what might break.</li>
</ul>



<p class="wp-block-paragraph">With TDD, that &#8220;debugging&#8221; time disappears. You catch the logic error within three seconds of writing it. That is where the speed comes from. TDD isn&#8217;t about writing code faster; it&#8217;s about <strong>shipping with confidence</strong> because your test suite is your documentation.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-fa4a4fd63c8b7bca9ef1945d2470b7a8">Final Thoughts for 2026</h3>



<p class="wp-block-paragraph">Modern Laravel development is about clean, maintainable logic. If you want to survive a project that grows over several years, you cannot rely on memory or manual testing.</p>



<p class="wp-block-paragraph">TDD isn&#8217;t a religion; it&#8217;s a discipline. Start today. Pick one small class or service. Write the test, watch it fail, and then write the code to fix it. Over time, it won&#8217;t feel like &#8220;work&#8221;—it will feel like having a second pair of eyes constantly reviewing your code as you type.</p>



<p class="wp-block-paragraph">Happy coding.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/06/22/tdd-in-laravel/">Test-Driven Development in Laravel: The Red-Green-Refactor Loop</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/06/22/tdd-in-laravel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Beyond Fat Controllers: Mastering Event-Driven Decoupling in Laravel</title>
		<link>https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/</link>
					<comments>https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 15 Jun 2026 18:00:00 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3295</guid>

					<description><![CDATA[<p>As your Laravel application scales, your controllers often evolve into a &#8220;dumping ground&#8221; for business logic. You start with a straightforward registration flow, and before you know it, you are juggling email notifications, Slack alerts, audit logging, and third-party API calls—all packed into a single method. This is the classic &#8220;Fat Controller&#8221; symptom. It makes [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/">Beyond Fat Controllers: Mastering Event-Driven Decoupling in Laravel</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">As your Laravel application scales, your controllers often evolve into a &#8220;dumping ground&#8221; for business logic. You start with a straightforward registration flow, and before you know it, you are juggling email notifications, Slack alerts, audit logging, and third-party API calls—all packed into a single method.</p>



<p class="wp-block-paragraph">This is the classic &#8220;Fat Controller&#8221; symptom. It makes your code fragile, nearly impossible to unit test, and violates the Single Responsibility Principle. But how do you solve this without introducing unnecessary enterprise-grade complexity? The answer lies in <strong>Event-Driven Architecture</strong>, kept simple and practical.</p>



<p class="wp-block-paragraph">Previous article in this category: <a href="https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/</a></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-474370956d7709da3ec17f71157bc840">The Hidden Cost of Tight Coupling</h2>



<p class="wp-block-paragraph">Consider a typical registration process in a Laravel 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(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 register(RegisterRequest $request)
{
    $user = User::create($request->validated());

    // Tightly coupled dependencies
    Mail::to($user)->send(new WelcomeEmail($user));
    Log::info('User registered: ' . $user->id);
    Analytics::track('user_signup', &#91;'id' => $user->id&#93;);
    Newsletter::subscribe($user->email);

    return response()->json(&#91;'message' => 'Success'&#93;, 201);
}
</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">register</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">RegisterRequest</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: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">validated</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// Tightly coupled dependencies</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">to</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">send</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">WelcomeEmail</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">info</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;User registered: &#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</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">    </span><span style="color: #4EC9B0">Analytics</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">track</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;user_signup&#39;</span><span style="color: #D4D4D4">, &#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Newsletter</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">subscribe</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">email</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">(&#91;</span><span style="color: #CE9178">&#39;message&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Success&#39;</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #B5CEA8">201</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 class="wp-block-paragraph">This code is intuitive, yet architecturally toxic. The controller is burdened with infrastructure knowledge. If your <code>Newsletter</code> API slows down, your user registration experiences lag. If the <code>Mail</code> service throws an exception, the entire request fails, potentially causing data inconsistency or frustration. You have created a <strong>brittle system</strong> where the success of a core operation depends on four secondary services.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-0089265c76ad3288311d4fce41a2359c">The KISS Philosophy: Events and Listeners</h2>



<p class="wp-block-paragraph">The KISS (Keep It Simple, Stupid) principle dictates that we should avoid over-engineering. In Laravel, you don’t need a massive message broker like RabbitMQ or Kafka to achieve decoupling. Laravel’s built-in Event system is perfect for 95% of use cases.</p>



<p class="wp-block-paragraph">Events act as an intermediary layer. Your controller simply broadcasts the fact that an action occurred, and various listeners react independently.</p>



<h3 class="wp-block-heading">1. Defining the Event</h3>



<p class="wp-block-paragraph">Think of an event as a &#8220;data transfer object&#8221; that carries necessary context.</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 UserRegistered
{
    public function __construct(public User $user) {}
}
</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">UserRegistered</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 style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$user</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">2. The Lean Controller</h3>



<p class="wp-block-paragraph">Now, look at how the controller looks after refactoring:</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 register(RegisterRequest $request)
{
    $user = User::create($request->validated());
    
    // Announce the action
    UserRegistered::dispatch($user);

    return response()->json(&#91;'message' => 'Success'&#93;, 201);
}
</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">register</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">RegisterRequest</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: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">validated</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">// Announce the action</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">UserRegistered</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</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">(&#91;</span><span style="color: #CE9178">&#39;message&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Success&#39;</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #B5CEA8">201</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 class="wp-block-paragraph">By decoupling, the controller is now focused solely on persistence and orchestration, not on the side effects.</p>



<h3 class="wp-block-heading">3. Implementing Asynchronous Listeners</h3>



<p class="wp-block-paragraph">The true power of events in Laravel emerges when you utilize background processing. By implementing the <code>ShouldQueue</code> interface, you move the heavy lifting away from the HTTP request cycle.</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 SendWelcomeEmail implements ShouldQueue
{
    public function handle(UserRegistered $event)
    {
        Mail::to($event->user)->send(new WelcomeEmail($event->user));
    }
}
</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">SendWelcomeEmail</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">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">UserRegistered</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$event</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">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">to</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$event</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">user</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">send</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">WelcomeEmail</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$event</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">user</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>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-aa4270b06ccc20494d613678b165da4e">Why This Architecture Scales</h2>



<p class="wp-block-paragraph">When you embrace events, you gain more than just cleaner controllers; you gain system resilience:</p>



<ol start="1" class="wp-block-list">
<li><strong>Fault Tolerance:</strong> If your <code>Newsletter</code> service is temporarily unavailable, the listener can automatically retry the task without affecting the user&#8217;s registration.</li>



<li><strong>Performance:</strong> The user receives a 201 response immediately, while secondary tasks (like analytics) run in the background.</li>



<li><strong>Extensibility:</strong> Need to add a &#8220;Push Notification&#8221; when a user registers? Just create a new listener. You don’t need to touch the registration controller logic at all, which eliminates the risk of regression bugs.</li>



<li><strong>Testing Contracts:</strong> You can now test the registration process by asserting that the <code>UserRegistered</code> event was fired, without needing to mock complex external mail or log services.</li>
</ol>



<p class="has-vivid-cyan-blue-color has-text-color has-link-color has-large-font-size wp-elements-6e66e7628d8c76c4849690f69643dfb7 wp-block-paragraph">Sandbox Example</p>



<p class="wp-block-paragraph"><strong>Theory is one thing, but code in action speaks louder. [Here you can see a live example] of what EventDispatcher looks like in an isolated environment and try out how easy it is to add a new listener without changing the core logic</strong>.</p>



<p class="wp-block-paragraph"><strong>Try this sandbox example by yourself</strong>: <a href="https://onlinephp.io/c/1f7b2">https://onlinephp.io/c/1f7b2</a></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-347de5baa9344552fb7f102f2baeb54d">Managing Complexity: The &#8220;Event Hell&#8221; Warning</h2>



<p class="wp-block-paragraph">While events are powerful, they are not a silver bullet. An excess of events—or &#8220;Event Hell&#8221;—can lead to a &#8220;spaghetti&#8221; flow where the execution path is impossible to track.</p>



<p class="wp-block-paragraph"><strong>Follow these best practices to maintain sanity:</strong></p>



<ul class="wp-block-list">
<li><strong>Don&#8217;t Over-Abstract:</strong> If a piece of code is used only in one place and will never change, don&#8217;t create an event. A simple function call is more readable.</li>



<li><strong>Focus on Side Effects:</strong> Events are for <em>post-processing</em>. Do not use events for core logic that <em>must</em> happen synchronously for the application to function.</li>



<li><strong>Descriptive Naming:</strong> Use past-tense names (<code>OrderPlaced</code>, <code>InvoiceGenerated</code>). This clearly signifies that the action has already been successfully committed to the database.</li>
</ul>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-c64b073238b062e615d362894267b462">Deep Dive: Events vs. Model Observers</h2>



<p class="wp-block-paragraph">A frequent question is: &#8220;When should I use Eloquent Observers instead of Events?&#8221;</p>



<p class="wp-block-paragraph"><strong>Eloquent Observers</strong> are strictly tied to database lifecycle events (e.g., <code>created</code>, <code>updated</code>, <code>deleted</code>). They are excellent when the side effect is <em>always</em> tied to a database change. <strong>Events</strong>, however, are more abstract. They represent <em>business domain actions</em>. An event like <code>UserLoggedIn</code> or <code>OrderShipped</code> is much more meaningful than a generic <code>updated</code> observer. Choose Events for business intent; use Observers for low-level database consistency.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-ea4b1f7b9ef294c8f79797b4aa279e63">Handling Failures in Background Jobs</h2>



<p class="wp-block-paragraph">When you move to <code>ShouldQueue</code>, you must plan for failure. In Laravel, you can define how your listeners handle retries:</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 $tries = 3;
public $backoff = 60; // Wait 60 seconds between retries
</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: #9CDCFE">$tries</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">3</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$backoff</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">60</span><span style="color: #D4D4D4">; </span><span style="color: #6A9955">// Wait 60 seconds between retries</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 class="wp-block-paragraph">This ensures that temporary outages (like a flickering API connection) don&#8217;t result in lost data.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-1c76ec62891917a367045fbf6ec06018">Final Thoughts</h2>



<p class="wp-block-paragraph">Refactoring to an event-driven design is not about making your code &#8220;fancy.&#8221; It is about <strong>durability</strong>. By insulating your core controllers from volatile external dependencies, you create a codebase that is easier to debug, faster to test, and significantly more adaptable to future requirements. Start small: identify one noisy side effect in your largest controller, and move it into a listener today. Your future self—and your servers—will thank you.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/">Beyond Fat Controllers: Mastering Event-Driven Decoupling in Laravel</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to Deploy 10 Times a Day Safely with Feature Flags</title>
		<link>https://codecraftdiary.com/2026/06/08/feature-flags-deployment/</link>
					<comments>https://codecraftdiary.com/2026/06/08/feature-flags-deployment/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 08 Jun 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[Development Workflow & Best Practices]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[software-design]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3290</guid>

					<description><![CDATA[<p>If you’ve been following my previous posts, you know I’m a big advocate for Trunk-Based Development and shrinking your pull requests until they almost feel too small. In a perfect world, developers merge code directly into the main branch multiple times a day, everything flows smoothly, and production remains rock solid. But let’s be honest. [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/06/08/feature-flags-deployment/">How to Deploy 10 Times a Day Safely with Feature Flags</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If you’ve been following my previous posts, you know I’m a big advocate for Trunk-Based Development and shrinking your pull requests until they almost feel too small. In a perfect world, developers merge code directly into the main branch multiple times a day, everything flows smoothly, and production remains rock solid.</p>



<p class="wp-block-paragraph">But let’s be honest. When you actually try to pitch this to a backend team working on a core system, you almost always hit the exact same wall of resistance.</p>



<p class="wp-block-paragraph">Someone in the back of the room will inevitably raise their hand and ask: <em>“That sounds great in theory, but I’m currently refactoring our legacy checkout service. It’s going to take me four days of deep architectural changes. Are you seriously telling me I should merge half-baked, broken code into the main trunk and push it straight to production where real customers are buying our products?”</em></p>



<p class="wp-block-paragraph">It’s a completely valid objection. If your only tool for hiding uncompleted work is holding onto a massive, long-lived feature branch, then trunk-based development breaks down immediately. You end up with the exact nightmare we talked about earlier: huge code reviews, painful merge conflicts, and code that rots before it ever sees a live environment.</p>



<p class="wp-block-paragraph">To make continuous delivery actually work without causing catastrophic production outages every single afternoon, you need to decouple two concepts that most engineering teams mistakenly treat as the exact same thing: <strong>Deployment</strong> and <strong>Release</strong>.</p>



<p class="wp-block-paragraph">Last article in this category is focused on Trunk-Based Development: <a href="https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/</a></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-2e9e01f9409e7798025615f340cba36d">The Core Concept: Shifting Left by Decoupling</h2>



<p class="wp-block-paragraph">In traditional development setups, deploying code and releasing a feature happen simultaneously. You merge your giant feature branch, the CI/CD pipeline runs, the code hits the live servers, and boom—your users immediately see the new functionality.</p>



<p class="wp-block-paragraph">This model is incredibly high-stakes. If something goes wrong, your only options are rolling back the entire deployment (which might contain unrelated fixes from other developers) or rushing a frantic hotfix through the pipeline while customer support tickets pile up and management starts breathing down your neck.</p>



<p class="wp-block-paragraph">Feature flags (or feature toggles) completely change this dynamic by shifting the risk layout.</p>



<ul class="wp-block-list">
<li><strong>Deployment</strong> means moving bits to servers. Your code lives in the production environment, executing safely under the hood, but it remains invisible or inaccessible to the end user. It’s a technical activity.</li>



<li><strong>Release</strong> means making that code active for users. It’s a business decision, completely independent of the deployment schedule.</li>
</ul>



<p class="wp-block-paragraph">By wrapping your new code inside a simple conditional statement, you can safely deploy unfinished logic to production ten times a day. The code is physically there on your production servers, but the execution path is dormant. You’ve successfully removed the stress from the deployment process.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-ca718fed50fdf9f7db61dd2c3d8cb6e1">A Realistic Look at the Code</h2>



<p class="wp-block-paragraph">Let’s skip the over-engineered enterprise frameworks for a moment and look at how this actually plays out in a standard backend context. Imagine you are upgrading a legacy payment gateway integration to a new, more reliable third-party API provider.</p>



<p class="wp-block-paragraph">Instead of waiting weeks to swap the entire implementation out in one massive, terrifying PR, you introduce a flag. In its simplest form, it looks something 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>public class PaymentProcessor {
    private final NewPaymentGateway newGateway;
    private final LegacyPaymentGateway legacyGateway;
    private final FeatureFlagClient flagClient;

    public void processPayment(Order order) {
        try {
            if (flagClient.isFeatureEnabled("use-new-payment-gateway", order.getUserId())) {
                newGateway.charge(order);
            } else {
                legacyGateway.charge(order);
            }
        } catch (Exception e) {
            // Fallback safety net
            if (flagClient.isFeatureEnabled("use-new-payment-gateway", order.getUserId())) {
                logger.warn("New gateway failed, falling back to legacy for user: " + order.getUserId(), e);
                legacyGateway.charge(order);
            } else {
                throw e;
            }
        }
    }
}
</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">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PaymentProcessor</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">final</span><span style="color: #D4D4D4"> NewPaymentGateway newGateway;</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">final</span><span style="color: #D4D4D4"> LegacyPaymentGateway legacyGateway;</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">final</span><span style="color: #D4D4D4"> FeatureFlagClient flagClient;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> void </span><span style="color: #DCDCAA">processPayment</span><span style="color: #D4D4D4">(Order order) {</span></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: #C586C0">if</span><span style="color: #D4D4D4"> (flagClient</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">isFeatureEnabled</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;use-new-payment-gateway&quot;</span><span style="color: #D4D4D4">, order</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">getUserId</span><span style="color: #D4D4D4">())) {</span></span>
<span class="line"><span style="color: #D4D4D4">                newGateway</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">charge</span><span style="color: #D4D4D4">(order);</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">                legacyGateway</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">charge</span><span style="color: #D4D4D4">(order);</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">Exception</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">e</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #6A9955">// Fallback safety net</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (flagClient</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">isFeatureEnabled</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;use-new-payment-gateway&quot;</span><span style="color: #D4D4D4">, order</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">getUserId</span><span style="color: #D4D4D4">())) {</span></span>
<span class="line"><span style="color: #D4D4D4">                logger</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">warn</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;New gateway failed, falling back to legacy for user: &quot;</span><span style="color: #D4D4D4"> + order</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">getUserId</span><span style="color: #D4D4D4">(), e);</span></span>
<span class="line"><span style="color: #D4D4D4">                legacyGateway</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">charge</span><span style="color: #D4D4D4">(order);</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: #C586C0">throw</span><span style="color: #D4D4D4"> e;</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 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 class="wp-block-paragraph">Notice that we aren&#8217;t just checking a global <code>true/false</code> boolean config value. We are passing the <code>order.getUserId()</code> into the flag client. This allows for runtime evaluation based on context.</p>



<p class="wp-block-paragraph">With this setup, you can merge your new gateway code when it’s only 20% finished. The interface is there, the basic structure is set, but the flag is turned off for everyone in production. You get to test your integration continuously against real staging environments or hidden production paths without risking a single actual customer transaction.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-47fca6d49c58b9185c92e1c176f89eb1">The Database Problem: Handling Migrations Safely</h2>



<p class="wp-block-paragraph">One common argument against frequent deployments with feature flags is: <em>“What about database changes? You can’t just feature-flag a schema migration.”</em> This is where many teams stumble. If your code depends on a new database column that doesn&#8217;t exist yet, your application will crash. To solve this, your database strategy must evolve alongside your code isolation. You have to follow the <strong>Expand and Contract</strong> pattern.</p>



<p class="wp-block-paragraph">Instead of renaming or modifying a column in a single destructive step, you break the change down into multiple backward-compatible deployments:</p>



<ol start="1" class="wp-block-list">
<li><strong>Expand:</strong> You deploy a migration that adds the new column or table. The old code doesn&#8217;t know it exists, so nothing breaks.</li>



<li><strong>Dual Write:</strong> You deploy a feature flag that starts writing data to <em>both</em> the old and new columns simultaneously, but still reads only from the old one. This ensures your new schema populates with live data.</li>



<li><strong>Backfill:</strong> You run a background script to copy historical data from the old structure to the new one.</li>



<li><strong>Flip the Switch:</strong> You change the feature flag to read from the new column. If performance degrades, you slide the flag back instantly.</li>



<li><strong>Contract:</strong> Once you are 100% confident, you remove the feature flag, delete the old code path, and deploy a final migration to drop the old column.</li>
</ol>



<p class="wp-block-paragraph">Yes, it requires more steps. But it transforms a terrifying database migration into a series of boring, completely safe tasks.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-fbcd923c2355fa25eae88c219062dcab">Moving Beyond Simple Booleans: Dark Launching and Canaries</h2>



<p class="wp-block-paragraph">Once you separate deployment from release, you unlock deployment workflows that make standard staging environments look completely obsolete. The most powerful of these is the <strong>Canary Release</strong> (or gradual rollout).</p>



<p class="wp-block-paragraph">Instead of flipping a switch and hoping your database doesn&#8217;t melt under a new query load, you can configure your feature flag system to evaluate based on percentages or specific user attributes. A realistic rollout plan for our new payment gateway looks like this:</p>



<h3 class="wp-block-heading">Phase 1: Internal Testing (The QA Tier)</h3>



<p class="wp-block-paragraph">The flag is enabled only for internal QA team user IDs or specifically whitelisted corporate IP addresses. You are running tests on the live production infrastructure, using real database connections, but nobody outside your company knows about it.</p>



<h3 class="wp-block-heading">Phase 2: The Canary (1% Traffic)</h3>



<p class="wp-block-paragraph">You route exactly 1% of random global traffic through the new gateway. You sit back and monitor your logging dashboard for an hour. You look for spikes in 500 errors, increased latency, or unusual database connection pool exhaustion. If 1% of your users experience a bug, it’s a minor issue you can catch quickly, rather than a company-wide outage affecting everyone.</p>



<h3 class="wp-block-heading">Phase 3: The Ramp-Up (10% -&gt; 50%)</h3>



<p class="wp-block-paragraph">If the metrics look clean after 24 hours, you scale the flag to 10%, then 50% over the next two days. This gradual increase helps you see how the system behaves under a realistic load.</p>



<h3 class="wp-block-heading">Phase 4: Full Release (100%)</h3>



<p class="wp-block-paragraph">The feature is stable, metrics are perfect, and the old legacy gateway is officially ready for decommissioning.</p>



<p class="wp-block-paragraph">If a subtle edge-case bug appears when you hit the 10% mark, you don’t panic. You don’t trigger a full rollback of the service container, which might take 15 minutes to compile and deploy. You simply log into your feature flag dashboard, slide the toggle back to 0%, and fix the bug at your own pace during normal working hours.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-fd33428a7bedc88ccc78a8f7352da4e2">The Dark Side: Managing the Architecture Debt</h2>



<p class="wp-block-paragraph">If you talk to any backend engineer who has used feature flags in a messy, fast-moving project, they will warn you about the exact same thing: technical debt. It is incredibly easy to treat feature flags like a magic wand, scattering them everywhere until your codebase looks like a tangled bowl of conditional spaghetti.</p>



<p class="wp-block-paragraph">If a flag stays in your code for six months after a feature has rolled out to 100% of users, it stops being a tool for continuous delivery and becomes an architectural liability. It makes the code harder to read, complicates unit testing because you have to mock multiple flag states, and leaves dead code paths hanging around indefinitely.</p>



<p class="wp-block-paragraph">To prevent your system from turning into an unmaintainable maze, you need to establish strict engineering discipline around the lifecycle of a flag.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-a609e2a8d270380ef56f648ca9d9128c">Treat Toggles as Temporary Scaffolding</h3>



<p class="wp-block-paragraph">Every time you create a feature flag, you should immediately create a corresponding ticket in your backlog to <em>remove</em> that flag. The definition of done for a new feature shouldn&#8217;t just be &#8220;it works in production.&#8221; It must be &#8220;it works in production, the old legacy code is deleted, and the flag conditional is completely stripped out of the codebase.&#8221;</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-a744a36d70fab3eeb6dd2058602316b4">Flag Owner Assignment</h3>



<p class="wp-block-paragraph">Every flag must have a clear owner—either a specific developer or a product team. If a flag sits unchanged for more than four weeks, the automated system or team lead should trigger a warning to review its status.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-b14dd75337feae3ab6be4d6c04e2f872">Keep Them Short-Lived</h3>



<p class="wp-block-paragraph">Release toggles should rarely live longer than a single development sprint or two. If a flag has been at 100% for more than a few days without complaints, it’s time to schedule a quick cleanup PR. Don’t let them turn into permanent configuration settings.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-69143eb89f1c5a93f97af2df27ca17c1">Choosing Your Tools Safely</h2>



<p class="wp-block-paragraph">You don&#8217;t need to build a massive, complex internal configuration platform from scratch to get started. For smaller teams, a simple, centralized database table or a Redis-backed configuration file that reloads dynamically can be enough to get your feet wet.</p>



<p class="wp-block-paragraph">As your team expands and you need advanced targeting rules, percentage rollouts, and audit logs, looking at dedicated tools like LaunchDarkly, Flagsmith, or open-source solutions like Unleash becomes highly valuable.</p>



<p class="wp-block-paragraph">The critical architectural requirement is that evaluating a flag must be lightning-fast. It cannot introduce a blocking HTTP request into your critical backend path every time a function is called; it needs to resolve locally in memory via cached flag states that sync asynchronously in the background.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-224ff3bdc0693e8fe64b2a8c5875049f">Final Thoughts</h2>



<p class="wp-block-paragraph">Transitioning to a workflow where you deploy to production ten times a day isn’t an engineering flex—it’s about reducing anxiety. It changes the entire culture of a development team. Production deployments stop being high-stress, late-night events that require everyone to be on standby with their laptops open. They become boring, routine non-events that happen continuously in the background while you grab a coffee or focus on your next task.</p>



<p class="wp-block-paragraph">Feature flags are the missing link that makes this possible. They give you the safety net to keep your pull requests tiny, your main branch green, and your delivery pipeline moving forward without ever breaking the user experience.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/06/08/feature-flags-deployment/">How to Deploy 10 Times a Day Safely with Feature Flags</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/06/08/feature-flags-deployment/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Flaky Tests in Laravel: Why Your CI Randomly Fails</title>
		<link>https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/</link>
					<comments>https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 15:00:00 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3285</guid>

					<description><![CDATA[<p>Your test suite passes locally.CI fails. You rerun the pipeline.Now everything is green. You change absolutely nothing.An hour later, another random failure appears. If this sounds familiar, you are probably dealing with flaky tests. Flaky tests are tests that sometimes pass and sometimes fail without any meaningful code changes. They are one of the most [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/">Flaky Tests in Laravel: Why Your CI Randomly Fails</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Your test suite passes locally.<br>CI fails.</p>



<p class="wp-block-paragraph">You rerun the pipeline.<br>Now everything is green.</p>



<p class="wp-block-paragraph">You change absolutely nothing.<br>An hour later, another random failure appears.</p>



<p class="wp-block-paragraph">If this sounds familiar, you are probably dealing with flaky tests.</p>



<p class="wp-block-paragraph">Flaky tests are tests that sometimes pass and sometimes fail without any meaningful code changes. They are one of the most frustrating problems in modern software development because they slowly destroy trust in your test suite.</p>



<p class="wp-block-paragraph">And once developers stop trusting tests, they start ignoring failures, rerunning pipelines blindly, and eventually shipping bugs to production.</p>



<p class="wp-block-paragraph">After dealing with flaky tests in multiple Laravel projects, I noticed something important:</p>



<p class="wp-block-paragraph">Most flaky tests are not caused by PHPUnit itself.</p>



<p class="wp-block-paragraph">They are usually caused by hidden shared state, timing assumptions, asynchronous behavior, or infrastructure leaking between tests.</p>



<p class="wp-block-paragraph">In this article, I’ll show the most common causes of flaky tests in Laravel and how to fix them properly.</p>



<p class="wp-block-paragraph"><strong>Previous article in Testing category:</strong> <a href="https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/ </a></p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-eb0457e0496adf750b601141a649a112">What Makes a Test “Flaky”?</h2>



<p class="wp-block-paragraph">A flaky test has three characteristics:</p>



<ul class="wp-block-list">
<li>It fails inconsistently</li>



<li>The failure is difficult to reproduce</li>



<li>Rerunning the test often “fixes” it</li>
</ul>



<p class="wp-block-paragraph">This is different from a normal failing test.</p>



<p class="wp-block-paragraph">A normal failing test indicates a deterministic bug.</p>



<p class="wp-block-paragraph">A flaky test creates uncertainty.</p>



<p class="wp-block-paragraph">And uncertainty is dangerous in CI pipelines because developers eventually stop taking failures seriously.</p>



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



<h1 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-b95ae98c62b7adf9274de6dcea29583f">1. Time-Dependent Tests</h1>



<p class="wp-block-paragraph">One of the most common sources of flaky tests is time.</p>



<p class="wp-block-paragraph">Laravel makes working with time easy through Carbon, but time-based logic can easily become unstable.</p>



<p class="wp-block-paragraph">Consider this 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(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_subscription_expires_after_24_hours(): void
{
    $subscription = Subscription::factory()->create(&#91;
        'expires_at' => now()->addDay(),
    &#93;);

    sleep(1);

    $this->assertFalse($subscription->isExpired());
}
</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_subscription_expires_after_24_hours</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: #9CDCFE">$subscription</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Subscription</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;expires_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">addDay</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: #DCDCAA">sleep</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">1</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">assertFalse</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$subscription</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isExpired</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 class="wp-block-paragraph">This test may pass most of the time.</p>



<p class="wp-block-paragraph">But depending on:</p>



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



<li>server load</li>



<li>execution timing</li>



<li>timezone handling</li>
</ul>



<p class="wp-block-paragraph">it can eventually fail unpredictably.</p>



<p class="wp-block-paragraph">The fix is simple:</p>



<p class="wp-block-paragraph">Use fixed time.</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>Carbon::setTestNow('2026-05-28 10:00:00');

$subscription = Subscription::factory()->create(&#91;
    'expires_at' => now()->addDay(),
&#93;);

$this->assertFalse($subscription->isExpired());
</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">Carbon</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">setTestNow</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;2026-05-28 10:00:00&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #9CDCFE">$subscription</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Subscription</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;expires_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">addDay</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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertFalse</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$subscription</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isExpired</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 class="wp-block-paragraph">And always clean up afterwards:</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>Carbon::setTestNow();
</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">Carbon</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">setTestNow</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 class="wp-block-paragraph">Without cleanup, fake time can leak into other tests and create even more randomness.</p>



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



<h1 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-49049bb62be2e31c13c67c338bf698ea">2. Shared Database State</h1>



<p class="wp-block-paragraph">Another massive source of flaky tests is database leakage between tests.</p>



<p class="wp-block-paragraph">I still see projects where tests depend on records created by previous tests.</p>



<p class="wp-block-paragraph">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>public function test_user_can_create_post(): void
{
    $this->post('/posts', &#91;
        'title' => 'Example',
    &#93;);

    $this->assertDatabaseCount('posts', 1);
}
</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_user_can_create_post</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">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/posts&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;title&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Example&#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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseCount</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;posts&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">1</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 class="wp-block-paragraph">At first, this looks harmless. However, once another test inserts posts into the database, the count may suddenly become 2, 5, or even 12.</p>



<p class="wp-block-paragraph">The fix is proper database isolation.</p>



<p class="wp-block-paragraph">In Laravel, this usually means:</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 class="wp-block-paragraph">or:</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 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">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 class="wp-block-paragraph">depending on your architecture.</p>



<p class="wp-block-paragraph">I already wrote an entire article comparing these approaches because using the wrong one can create hidden instability.</p>



<p class="wp-block-paragraph">The important part is this:</p>



<p class="wp-block-paragraph">Tests should never depend on leftovers from previous tests.</p>



<p class="wp-block-paragraph">Ever.</p>



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



<h1 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-57d4e0d08cd6c3419b4dd77082747f4e">3. Random Factories</h1>



<p class="wp-block-paragraph">Factories are great.</p>



<p class="wp-block-paragraph">Randomness is not.</p>



<p class="wp-block-paragraph">This test looks innocent:</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>$user = User::factory()->create();

$this->assertEquals('admin', $user->role);
</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertEquals</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;admin&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">role</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 class="wp-block-paragraph">But if the factory generates random roles, this test becomes unstable immediately.</p>



<p class="wp-block-paragraph">I’ve seen this problem especially in large Laravel projects where factories evolved over years and slowly accumulated randomness everywhere.</p>



<p class="wp-block-paragraph">Instead, explicitly define required 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(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>$user = User::factory()->create(&#91;
    'role' => 'admin',
&#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: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</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;role&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;admin&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">&#93;);</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 class="wp-block-paragraph">Deterministic data creates deterministic tests.</p>



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



<h1 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-7f4392a629c51aa0d6b5c4ab3948f6df">4. Queue and Async Problems</h1>



<p class="wp-block-paragraph">Queues are one of the biggest sources of flaky behavior.</p>



<p class="wp-block-paragraph">Especially when developers partially fake queues while still allowing some jobs to execute asynchronously.</p>



<p class="wp-block-paragraph">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>Queue::fake();

dispatch(new SendInvoiceJob($invoice));

$this->assertDatabaseHas('invoices', &#91;
    'status' => 'sent',
&#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: #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: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">SendInvoiceJob</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$invoice</span><span style="color: #D4D4D4">));</span></span>
<span class="line"></span>
<span class="line"><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;invoices&#39;</span><span style="color: #D4D4D4">, &#91;</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;sent&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">&#93;);</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 class="wp-block-paragraph">This can fail because the queued job never actually runs.</p>



<p class="wp-block-paragraph">Or worse:<br>it runs sometimes depending on environment configuration.</p>



<p class="wp-block-paragraph">Another common issue is testing behavior immediately after dispatching async jobs.</p>



<p class="wp-block-paragraph">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>dispatch(new SyncProductsJob());

$this->assertDatabaseCount('products', 500);
</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">SyncProductsJob</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseCount</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;products&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">500</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 class="wp-block-paragraph">The assertion may execute before the worker finishes.</p>



<p class="wp-block-paragraph">Locally it passes.</p>



<p class="wp-block-paragraph">In CI it randomly fails.</p>



<p class="wp-block-paragraph">A better approach is either:</p>



<ul class="wp-block-list">
<li>testing the dispatch itself,</li>



<li>or running jobs synchronously during tests.</li>
</ul>



<p class="wp-block-paragraph">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>Bus::fake();

dispatch(new SyncProductsJob());

Bus::assertDispatched(SyncProductsJob::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: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">SyncProductsJob</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">SyncProductsJob</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</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 class="wp-block-paragraph">Or:</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()->set('queue.default', 'sync');
</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">()-&gt;</span><span style="color: #DCDCAA">set</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;queue.default&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;sync&#39;</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 class="wp-block-paragraph">during the test environment.</p>



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



<h1 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-015e84f59cf15c581946036d7be67f82">5. Parallel Testing Issues</h1>



<p class="wp-block-paragraph">Parallel testing speeds up CI dramatically. However, it also exposes hidden shared state.</p>



<p class="wp-block-paragraph">I’ve seen failures caused by:</p>



<ul class="wp-block-list">
<li>shared Redis keys</li>



<li>shared files</li>



<li>cached config</li>



<li>temporary directories</li>



<li>static variables</li>



<li>singleton state</li>
</ul>



<p class="wp-block-paragraph">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>Storage::disk('local')->put('report.pdf', 'content');
</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">Storage</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">disk</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;local&#39;</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">put</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;report.pdf&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;content&#39;</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 class="wp-block-paragraph">If multiple tests write the same file simultaneously, random failures appear.</p>



<p class="wp-block-paragraph">The fix is isolation.</p>



<p class="wp-block-paragraph">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>Storage::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">Storage</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 class="wp-block-paragraph">or unique filenames:</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>$file = Str::uuid() . '.pdf';
</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">$file</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Str</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">uuid</span><span style="color: #D4D4D4">() </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #CE9178">&#39;.pdf&#39;</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 class="wp-block-paragraph">Parallel testing does not create flaky tests.</p>



<p class="wp-block-paragraph">It reveals problems that already existed.</p>



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



<h1 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-cc302a6a46a3765b3cc7ce99772fa3e7">6. External APIs</h1>



<p class="wp-block-paragraph">Real HTTP calls inside tests are dangerous.</p>



<p class="wp-block-paragraph">Sometimes the API is slow.</p>



<p class="wp-block-paragraph">Sometimes rate limits trigger.</p>



<p class="wp-block-paragraph">Sometimes sandbox environments fail.</p>



<p class="wp-block-paragraph">And suddenly your test suite becomes unreliable for reasons completely outside your application.</p>



<p class="wp-block-paragraph">This is why external APIs should usually be mocked or faked.</p>



<p class="wp-block-paragraph">Laravel provides excellent HTTP faking:</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>Http::fake([
    '*' => Http::response(&#91;
        'success' => true,
    &#93;, 200),
]);
</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">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">([</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;*&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;success&#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 style="color: #B5CEA8">200</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 class="wp-block-paragraph">Now your tests become:</p>



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



<li>deterministic</li>



<li>independent from network stability</li>
</ul>



<p class="wp-block-paragraph">I covered this topic in more detail in my API mocking article because external integrations are one of the easiest ways to accidentally create unstable tests.</p>



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



<h1 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-efe7916a663b18991f6124ca0494838c">7. Tests That Depend on Execution Order</h1>



<p class="wp-block-paragraph">This one is extremely dangerous.</p>



<p class="wp-block-paragraph">A test passes only because another test ran before it.</p>



<p class="wp-block-paragraph">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>public function test_admin_exists(): void
{
    $this->assertDatabaseHas('users', &#91;
        'email' => 'admin@example.com',
    &#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_admin_exists</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">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;users&#39;</span><span style="color: #D4D4D4">, &#91;</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;admin@example.com&#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>



<p class="wp-block-paragraph">This silently depends on another test creating the admin user first.</p>



<p class="wp-block-paragraph">Run tests individually and this suddenly fails.</p>



<p class="wp-block-paragraph">A good test should work:</p>



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



<li>repeatedly</li>



<li>in any order</li>
</ul>



<p class="wp-block-paragraph">If execution order matters, the suite is fragile.</p>



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



<h1 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-aca33574de875dc3904619ed5877ccc7">Why Flaky Tests Become Expensive</h1>



<p class="wp-block-paragraph">The biggest problem with flaky tests is not technical.</p>



<p class="wp-block-paragraph">It is psychological.</p>



<p class="wp-block-paragraph">Once developers stop trusting CI:</p>



<ul class="wp-block-list">
<li>failures get ignored</li>



<li>reruns become normal</li>



<li>real bugs get missed</li>



<li>confidence disappears</li>
</ul>



<p class="wp-block-paragraph">I’ve seen teams where developers reran pipelines three or four times automatically because “CI is always flaky anyway.”</p>



<p class="wp-block-paragraph">That is dangerous.</p>



<p class="wp-block-paragraph">Because eventually a real regression hides inside the noise.</p>



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



<h1 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-079151b90461299bb65eafd3cdeb41fd">Final Thoughts</h1>



<p class="wp-block-paragraph">Flaky tests are rarely random.</p>



<p class="wp-block-paragraph">There is almost always an underlying engineering problem:</p>



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



<li>uncontrolled time</li>



<li>async behavior</li>



<li>non-isolated infrastructure</li>



<li>hidden dependencies</li>
</ul>



<p class="wp-block-paragraph">The solution is not “rerun CI.”</p>



<p class="wp-block-paragraph">The solution is making tests deterministic.</p>



<p class="wp-block-paragraph">A reliable test suite should produce the same result every time:</p>



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



<li>in CI</li>



<li>on every machine</li>



<li>under every execution order</li>
</ul>



<p class="wp-block-paragraph">Once your tests become deterministic, your entire development workflow becomes faster, safer, and dramatically less frustrating.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/">Flaky Tests in Laravel: Why Your CI Randomly Fails</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>State Pattern vs. Enums in Modern PHP</title>
		<link>https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/</link>
					<comments>https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 25 May 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></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=3275</guid>

					<description><![CDATA[<p>In many PHP and Laravel applications, entity lifecycles start simple. An Order can be: When PHP introduced native Enums, they became the perfect fit for this kind of state modeling. They are type-safe, database-friendly, and much cleaner than arbitrary strings spread across the codebase. For simple workflows, Enums are often exactly the right solution. The [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/">State Pattern vs. Enums in Modern PHP</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In many PHP and Laravel applications, entity lifecycles start simple. An Order can be:</p>



<ul class="wp-block-list">
<li class="has-d-4-d-4-d-4-color has-text-color">Pending</li>



<li class="has-d-4-d-4-d-4-color has-text-color">Paid</li>



<li class="has-d-4-d-4-d-4-color has-text-color">Shipped</li>



<li class="has-d-4-d-4-d-4-color has-text-color">Cancelled</li>
</ul>



<p class="wp-block-paragraph">When PHP introduced native Enums, they became the perfect fit for this kind of state modeling. They are type-safe, database-friendly, and much cleaner than arbitrary strings spread across the codebase.</p>



<p class="wp-block-paragraph">For simple workflows, Enums are often exactly the right solution. The problem begins when states stop being just labels and start accumulating behavior.</p>



<p class="wp-block-paragraph">This article explores where Enums work well, where they start breaking down, and how the State Pattern can help without introducing unnecessary complexity or framework-heavy abstractions.</p>



<p class="wp-block-paragraph">Previous articlet in Refactoring cattegory: <a href="https://codecraftdiary.com/2026/05/02/mastering-value-objects-in-php/">https://codecraftdiary.com/2026/05/02/mastering-value-objects-in-php/</a></p>



<h2 class="wp-block-heading">Enums Are Excellent — Until They Aren’t</h2>



<p class="wp-block-paragraph">For simple workflows, Enums are clean and maintainable.</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>enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';
}
</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">enum</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStatus</span><span style="color: #D4D4D4">: </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Pending = </span><span style="color: #CE9178">&#39;pending&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Paid = </span><span style="color: #CE9178">&#39;paid&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Shipped = </span><span style="color: #CE9178">&#39;shipped&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Cancelled = </span><span style="color: #CE9178">&#39;cancelled&#39;</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 class="wp-block-paragraph">This is ideal when states are primarily used for:</p>



<ul class="wp-block-list">
<li>Filtering and querying data</li>



<li>Display logic and badges in UI</li>



<li>Basic validation</li>



<li>API serialization</li>



<li>Database persistence</li>
</ul>



<p class="wp-block-paragraph">Problems start appearing when business rules become state-dependent. A common first step is adding helper methods directly into the Enum:</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>enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    public function canBeCancelled(): bool
    {
        return match($this) {
            self::Pending, self::Paid => true,
            self::Shipped, self::Cancelled => false,
        };
    }
}
</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">enum</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStatus</span><span style="color: #D4D4D4">: </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Pending = </span><span style="color: #CE9178">&#39;pending&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Paid = </span><span style="color: #CE9178">&#39;paid&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Shipped = </span><span style="color: #CE9178">&#39;shipped&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Cancelled = </span><span style="color: #CE9178">&#39;cancelled&#39;</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">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">canBeCancelled</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">bool</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: #C586C0">match</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 style="color: #569CD6">self</span><span style="color: #D4D4D4">::Pending, </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Paid </span><span style="color: #569CD6">=&gt;</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: #569CD6">self</span><span style="color: #D4D4D4">::Shipped, </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Cancelled </span><span style="color: #569CD6">=&gt;</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>
<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 class="wp-block-paragraph">This is still perfectly reasonable and respects the KISS principle.</p>



<p class="wp-block-paragraph">But over time, workflows tend to evolve. A cancellation process may eventually require refunding payments, restocking inventory, notifying external systems, creating audit logs, or dispatching events.</p>



<p class="wp-block-paragraph">At that point, the Enum slowly stops being a simple value object and starts becoming a workflow engine.</p>



<h2 class="wp-block-heading">The Hidden Problem: Growing Coupling</h2>



<p class="wp-block-paragraph">Consider how a bloating Enum typically looks in production:</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>enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    public function cancel(
        Order $order,
        PaymentGateway $gateway,
        InventoryManager $inventory
    ): void {
        match ($this) {
            self::Pending => $order->updateStatus(self::Cancelled),

            self::Paid => $this->executeCancellationWithRefund($order, $gateway, $inventory),

            self::Shipped => throw new LogicException('Cannot cancel a shipped order.'),
            self::Cancelled => throw new LogicException('Order is already cancelled.'),
        };
    }

    private function executeCancellationWithRefund(Order $order, PaymentGateway $gateway, InventoryManager $inventory): void
    {
        $gateway->refund($order->payment_id);
        $inventory->restock($order->items);
        $order->updateStatus(self::Cancelled);
    }
}
</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">enum</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStatus</span><span style="color: #D4D4D4">: </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Pending = </span><span style="color: #CE9178">&#39;pending&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Paid = </span><span style="color: #CE9178">&#39;paid&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Shipped = </span><span style="color: #CE9178">&#39;shipped&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Cancelled = </span><span style="color: #CE9178">&#39;cancelled&#39;</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">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">cancel</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><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>
<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 style="color: #4EC9B0">InventoryManager</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</span></span>
<span class="line"><span style="color: #D4D4D4">    ): </span><span style="color: #569CD6">void</span><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: #569CD6">$this</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">::Pending </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">updateStatus</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Cancelled),</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Paid </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: #DCDCAA">executeCancellationWithRefund</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$inventory</span><span style="color: #D4D4D4">),</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Shipped </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">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Cannot cancel a shipped order.&#39;</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">::Cancelled </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">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Order is already cancelled.&#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>
<span class="line"></span>
<span class="line"><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">executeCancellationWithRefund</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: #4EC9B0">PaymentGateway</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">InventoryManager</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</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: #9CDCFE">$gateway</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">refund</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">payment_id</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$inventory</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">restock</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</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: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">updateStatus</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Cancelled);</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 class="wp-block-paragraph">The issue here is not the number of lines. <strong>The issue is coupling.</strong></p>



<p class="wp-block-paragraph">The Enum now knows about payment infrastructure, inventory management, business transitions, and side effects. Adding a new state such as <code>PartiallyRefunded</code> now requires modifying a growing conditional structure that centralizes unrelated responsibilities.</p>



<p class="wp-block-paragraph">This is where applications experience <strong>state explosion</strong>—the point where transitions and side effects become increasingly difficult to isolate and reason about. At this stage, the code may still look “short,” but it is no longer simple.</p>



<h2 class="wp-block-heading">The State Pattern: Isolating Behavior</h2>



<p class="wp-block-paragraph">The State Pattern addresses this by moving behavior into dedicated state objects. Instead of one large conditional structure, each state becomes responsible for its own transitions and rules.</p>



<h3 class="wp-block-heading">1. Define the Workflow Contract</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>interface OrderState
{
    public function cancel(Order $order): void;
    public function ship(Order $order): void;
    public function toValue(): string;
}
</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">interface</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderState</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">cancel</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 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">ship</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 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">toValue</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">string</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 class="wp-block-paragraph">The interface should stay minimal. Only include operations whose behavior actually changes depending on the state.</p>



<h3 class="wp-block-heading">2. Create Small, Focused State Classes</h3>



<p class="wp-block-paragraph">Each state becomes an isolated, testable component. Look at how clean the responsibilities become:</p>



<h4 class="wp-block-heading">Pending State</h4>



<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>readonly class PendingState implements OrderState
{
    public function cancel(Order $order): void
    {
        $order->transitionTo(new CancelledState());
    }

    public function ship(Order $order): void
    {
        throw new LogicException('Cannot ship an unpaid order.');
    }

    public function toValue(): string
    {
        return 'pending';
    }
}
</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">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PendingState</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderState</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">cancel</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: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">transitionTo</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CancelledState</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">ship</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">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Cannot ship an unpaid order.&#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: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">toValue</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">string</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: #CE9178">&#39;pending&#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>
<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>



<h4 class="wp-block-heading">Paid State</h4>



<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>readonly class PaidState implements OrderState
{
    public function __construct(
        private PaymentGateway $gateway,
        private InventoryManager $inventory,
    ) {}

    public function cancel(Order $order): void
    {
        $this->gateway->refund($order->payment_id);
        $this->inventory->restock($order->items);
        
        $order->transitionTo(new CancelledState());
    }

    public function ship(Order $order): void
    {
        $order->transitionTo(new ShippedState());
    }

    public function toValue(): string
    {
        return 'paid';
    }
}
</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">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PaidState</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderState</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">InventoryManager</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</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">cancel</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: #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">refund</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">payment_id</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">inventory</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">restock</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</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>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">transitionTo</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CancelledState</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">ship</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: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">transitionTo</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShippedState</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">toValue</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">string</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: #CE9178">&#39;paid&#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>
<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>



<h4 class="wp-block-heading">Shipped State</h4>



<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>readonly class ShippedState implements OrderState
{
    public function cancel(Order $order): void
    {
        throw new LogicException('The order has already been shipped.');
    }

    public function ship(Order $order): void
    {
        throw new LogicException('Order is already shipped.');
    }

    public function toValue(): string
    {
        return 'shipped';
    }
}
</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">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShippedState</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderState</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">cancel</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">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;The order has already been shipped.&#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: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">ship</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">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Order is already shipped.&#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: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">toValue</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">string</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: #CE9178">&#39;shipped&#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>
<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 class="wp-block-paragraph">Most importantly, adding a new state no longer requires modifying a massive conditional block. This aligns naturally with the <strong>Open/Closed Principle</strong>.</p>



<h2 class="wp-block-heading">3. A Pragmatic Hybrid Approach</h2>



<p class="wp-block-paragraph">Completely replacing Enums with raw state objects is often unnecessary in database-driven applications. In practice, the most maintainable approach is a hybrid architecture:</p>



<ul class="wp-block-list">
<li><strong>Enums</strong> handle persistence, transport, and API serialization.</li>



<li><strong>State objects</strong> handle business behavior and side effects.</li>
</ul>



<p class="wp-block-paragraph">The Enum remains the canonical storage format:</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>enum OrderStatusEnum: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';
}
</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">enum</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">: </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Pending = </span><span style="color: #CE9178">&#39;pending&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Paid = </span><span style="color: #CE9178">&#39;paid&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Shipped = </span><span style="color: #CE9178">&#39;shipped&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Cancelled = </span><span style="color: #CE9178">&#39;cancelled&#39;</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">Resolving State Behavior Cleanly</h3>



<p class="wp-block-paragraph">Instead of resolving dependencies directly inside the model, a dedicated factory keeps infrastructure concerns isolated from your domain.</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>readonly class OrderStateFactory
{
    public function __construct(
        private PaymentGateway $gateway,
        private InventoryManager $inventory,
    ) {}

    public function make(OrderStatusEnum $status): OrderState
    {
        return match ($status) {
            OrderStatusEnum::Pending => new PendingState(),
            OrderStatusEnum::Paid => new PaidState($this->gateway, $this->inventory),
            OrderStatusEnum::Shipped => new ShippedState(),
            OrderStatusEnum::Cancelled => new CancelledState(),
        };
    }
}
</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">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStateFactory</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">InventoryManager</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</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">make</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$status</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">OrderState</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: #C586C0">match</span><span style="color: #D4D4D4"> (</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">OrderStatusEnum</span><span style="color: #D4D4D4">::Pending </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PendingState</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">::Paid </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PaidState</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">, </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">inventory</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">::Shipped </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShippedState</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">::Cancelled </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CancelledState</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 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">Keeping the Model Lightweight</h3>



<p class="wp-block-paragraph">Now, the <code>Order</code> model stays clean and decoupled from infrastructure services. It simply orchestrates the workflow via the factory:</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 OrderStatusEnum $status;
    public int $payment_id;
    public array $items = [];

    public function __construct(
        private readonly OrderStateFactory $stateFactory,
    ) {}

    public function transitionTo(OrderState $state): void
    {
        $this->status = OrderStatusEnum::from($state->toValue());
    }

    public function cancel(): void
    {
        $this->stateFactory
            ->make($this->status)
            ->cancel($this);
    }

    public function ship(): void
    {
        $this->stateFactory
            ->make($this->status)
            ->ship($this);
    }
}
</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: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$status</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">$payment_id</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>
<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: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStateFactory</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$stateFactory</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">transitionTo</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderState</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$state</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">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">status</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">from</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$state</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">toValue</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">cancel</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">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stateFactory</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">make</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</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">            -&gt;</span><span style="color: #DCDCAA">cancel</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">ship</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">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stateFactory</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">make</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</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">            -&gt;</span><span style="color: #DCDCAA">ship</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 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 class="wp-block-paragraph">This keeps your persistence simple, business logic isolated, dependencies explicit, and workflows extensible—all without introducing heavy framework abstractions.</p>



<h2 class="wp-block-heading">Testing Becomes Significantly Easier</h2>



<p class="wp-block-paragraph">One of the biggest advantages of state objects is test isolation. Instead of testing a large Enum with multiple branches and heavy mocking setups, each workflow state can be verified independently:</p>



<ul class="wp-block-list">
<li><code>PendingStateTest</code> — Verify that cancellation transitions directly to cancelled.</li>



<li><code>PaidStateTest</code> — Assert that the payment gateway receives the refund call and inventory is restocked.</li>



<li><code>ShippedStateTest</code> — Assert that exceptions are thrown correctly on forbidden actions.</li>
</ul>



<p class="wp-block-paragraph">This dramatically reduces test setup complexity and makes transition rules much easier to verify.</p>



<h2 class="wp-block-heading">When Should You Use Enums vs. State Objects?</h2>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Use Enums when:</strong></p>



<ul class="wp-block-list">
<li>The state is primarily a static label.</li>



<li>Transitions are simple and linear.</li>



<li>Behavior differences between states are minimal.</li>



<li>No external services or infrastructure are involved.</li>



<li><em>Examples:</em> Blog post status (<code>Draft</code>, <code>Published</code>), user visibility flags, or filtering categories.</li>
</ul>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Use the State Pattern when:</strong></p>



<ul class="wp-block-list">
<li>Transitions trigger side effects or external APIs.</li>



<li>States require completely different validation rules.</li>



<li>Workflows keep expanding with new edge cases.</li>



<li>Conditional logic (<code>match</code> or <code>if/else</code>) around the same state starts repeating across the codebase.</li>



<li><em>Examples:</em> Payment workflows, fulfillment systems, subscription lifecycles, or approval pipelines.</li>
</ul>
</blockquote>



<h2 class="wp-block-heading">Final Thoughts</h2>



<p class="wp-block-paragraph">Enums are not the enemy. In fact, they are often the best solution for simple state representation.</p>



<p class="wp-block-paragraph">The real problem starts when business workflows evolve and a single Enum begins accumulating infrastructure dependencies, transition orchestration, side effects, and validation logic. At that point, the issue is no longer code length—it is <strong>responsibility density</strong>.</p>



<p class="wp-block-paragraph">The State Pattern is valuable not because it is “more advanced,” but because it isolates change. And in long-lived systems, isolated change is usually what keeps complexity manageable.</p>
<p>The post <a href="https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/">State Pattern vs. Enums in Modern PHP</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Trunk-Based Development: From Chaos to Flow</title>
		<link>https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/</link>
					<comments>https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 18 May 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Development Workflow & Best Practices]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[devops]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[workflow]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3268</guid>

					<description><![CDATA[<p>If you’ve followed the first two parts of this series, you know the hard truth: most teams aren’t actually doing Trunk-Based Development. They are doing &#8220;Short-lived Feature Branching&#8221; with better branding. We’ve talked about why your PRs are still too big and why the 6-month pull request is a parallel universe that kills delivery. But [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/">Trunk-Based Development: From Chaos to Flow</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-33">If you’ve followed the first two parts of this series, you know the hard truth: most teams aren’t actually doing Trunk-Based Development<sup></sup>. They are doing &#8220;Short-lived Feature Branching&#8221; with better branding<sup></sup><sup></sup><sup></sup><sup></sup>. We’ve talked about why your PRs are still too big <sup></sup>and why the 6-month pull request is a parallel universe that kills delivery<sup></sup><sup></sup><sup></sup><sup></sup>.</p>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-34">But how do you actually fix it? Knowing small PRs are better is easy. Changing a team&#8217;s habits is the hard part.<br>This is the practical roadmap for moving from long-lived branches to real trunk-based flow.</p>



<p class="wp-block-paragraph">Previous posts about Trunk-Based Development: Pt. 2 &#8211;<a href="https://codecraftdiary.com/2026/04/29/trunk-based-development-your-pull-requests-are-still-too-big/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/04/29/trunk-based-development-your-pull-requests-are-still-too-big/</a></p>



<p class="wp-block-paragraph">Pt. 1 <a href="https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/</a></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-b4a2e9edbf137201d675742e19120e9e">Phase 1: Fixing the Foundation (The Infrastructure)</h2>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-35">You cannot do trunk-based development with slow pipelines<sup></sup>. If your CI/CD suite is a bottleneck, your developers will naturally revert to batching work to &#8220;save time&#8221;<sup></sup><sup></sup><sup></sup>.</p>



<h3 class="wp-block-heading">1. The 10-Minute Rule for CI</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-36">In 2026, speed is a requirement, not a luxury<sup></sup>. If your CI takes 20 minutes, there is friction; if it takes 60 minutes, people stop merging frequently<sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>Target:</strong> Aim for CI under 10 minutes.</li>



<li><strong>Action:</strong> Parallelize your test suites. If a test is flaky, don&#8217;t ignore it—fix it or delete it. A flaky test suite is a debt that destroys the confidence needed for frequent merges.</li>
</ul>



<h3 class="wp-block-heading">2. Radical Observability</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-38">To merge to main multiple times a day safely, you need to know exactly what is happening in production<sup></sup><sup></sup><sup></sup><sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>Action:</strong> Implement real-time logging and alerting. If you merge a small change and the error rate spikes, you should know within seconds, not after a customer support ticket arrives.</li>
</ul>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-29c24c10968e70f2c22e07e24b09bae3">Phase 2: Mastering the Tools of &#8220;Incomplete&#8221; Work</h2>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-39">The biggest fear in TBD is merging work that isn&#8217;t &#8220;done&#8221;<sup></sup>. To overcome this, you must decouple <strong>deployment</strong> from <strong>release</strong><sup></sup><sup></sup>.</p>



<h3 class="wp-block-heading">3. Feature Flags as a Standard</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-40">Feature flags are the missing piece for most teams<sup></sup><sup></sup>. They allow you to merge partial work and control exposure without waiting for the entire feature to be polished<sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>The Strategy:</strong> Wrap new logic in a toggle. This lets the code live in the main branch, deployed to production but hidden from users until it&#8217;s ready.</li>



<li><strong>The Rule:</strong> A feature flag must exist from the start, not as an afterthought.</li>
</ul>



<h3 class="wp-block-heading">4. Branch by Abstraction for Large Changes</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-43">When you are doing a complete architectural overhaul, do not create a &#8220;v2-architecture&#8221; Git branch<sup></sup>. That is a recipe for a merge nightmare<sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>The Strategy:</strong> Keep both the old and new architectures in the main branch simultaneously.</li>



<li><strong>The Execution:</strong> Use an abstraction layer (an interface or wrapper) to toggle between the old and new logic. Run &#8220;dark launches&#8221; where the new code executes, but you ignore the results or simply compare them against the old version to gain confidence.</li>
</ul>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-da50a52945aa1f7338c551e98359bfba">Phase 3: Rewiring the Process (Small PRs)</h2>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-46">Small pull requests are the backbone of TBD<sup></sup><sup></sup><sup></sup><sup></sup>. A large PR is a cognitive nightmare that leads to shallow reviews and delayed deployments<sup></sup>.</p>



<h3 class="wp-block-heading">5. Enforce Hard PR Size Limits</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-47">Don&#8217;t make small PRs a suggestion; make them a constraint<sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>The Rule:</strong> Set a soft limit of ~300 lines and a hard limit of ~400–500 lines per PR.</li>



<li><strong>The Logic:</strong> Constraints force better behavior. If a task is too big, it forces the developer to think about how to slice it vertically.</li>
</ul>



<h3 class="wp-block-heading">6. Vertical vs. Horizontal Slicing</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-50">Stop splitting work by &#8220;Backend PR,&#8221; &#8220;Database PR,&#8221; and &#8220;Frontend PR&#8221;<sup></sup><sup></sup>. This creates artificial dependencies and forces you to wait until all are done before merging<sup></sup><sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>The Strategy:</strong> Slice vertically. One PR should deliver a minimal end-to-end functionality (even if hidden by a flag), followed by another PR that extends that behavior.</li>
</ul>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-b64911a58c1530395a27c6aa2d2277f9">Phase 4: The Cultural Shift (The Hard Part)</h2>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-52">Trunk-based development is 10% tooling and 90% discipline<sup></sup><sup></sup><sup></sup><sup></sup>.</p>



<h3 class="wp-block-heading">7. Optimize for Review Speed, Not Just Quality</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-53">If reviews take days, developers will batch work to avoid the &#8220;waiting tax&#8221;<sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>Expectation:</strong> A PR should be reviewed within a few hours, not days.</li>



<li><strong>Technique:</strong> If a PR is small (under 300 &#8211; 400 lines), it takes 15 minutes to review. If it’s still stuck, do live reviews or pair programming to clear the logjam.</li>
</ul>



<h3 class="wp-block-heading">8. Accept &#8220;Ugly but Correct&#8221;</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-56">Flow matters more than perfection<sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>The Mindset:</strong> It is better to merge a small, slightly imperfect (but safe) change today than a &#8220;perfect&#8221; massive change next week. You can refactor and improve incrementally once the code is integrated -> <strong>but don&#8217;t forget to refactor</strong>.</li>
</ul>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-719d0410563fdd8d55254b4f0db48f32">Phase 5: The Cleanup and AI Reality</h2>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-58">AI-assisted coding is generating more code than ever. If you don&#8217;t have small changes and fast integration, your workflow will collapse under the sheer volume of AI-generated changes.</p>



<h3 class="wp-block-heading">9. The Non-Optional Cleanup</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-59">Feature flags and abstractions are great, but they create technical debt if left forever<sup></sup><sup></sup><sup></sup><sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>The Rule:</strong> Once a rollout is 100% successful, deleting the old code and the flag is part of the original task, not a &#8220;nice-to-have&#8221; for later.</li>
</ul>



<h3 class="wp-block-heading">10. Track Your Behavior (Metrics)</h3>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-61">Stop guessing if you are doing TBD. Measure it<sup></sup><sup></sup>.</p>



<ul class="wp-block-list">
<li><strong>KPIs:</strong> Track average PR size, PR lifetime, and merges per developer per day.</li>



<li><strong>Reality Check:</strong> If your PRs live for days and contain thousands of lines, you are still doing feature-branch development.</li>
</ul>



<h3 class="wp-block-heading">Common Failure Mode</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Teams adopt feature flags but still keep long-lived branches.<br>That is not trunk-based development.<br>The branch lifetime matters more than the branching strategy itself.</p>
</blockquote>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-5aa7b167613851ef5f737ecc5b0682c8">Final Thought</h2>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-64">Trunk-based development feels uncomfortable at first because it goes against the natural desire to be &#8220;complete&#8221; and &#8220;polished&#8221; before sharing work<sup></sup>. It requires you to prioritize the <strong>system&#8217;s flow</strong> over your <strong>individual comfort</strong><sup></sup>.</p>



<p class="wp-block-paragraph" id="p-rc_e009b3fffe4fec9a-65">If you fix just one thing this month, make your pull requests radically smaller<sup></sup><sup></sup>. Faster reviews, fewer bugs, and smoother delivery will follow<sup></sup><sup></sup><sup></sup><sup></sup>. Everything else is just details.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/">Trunk-Based Development: From Chaos to Flow</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How Mutation Testing Exposes the Truth (PHP 2026 Edition)</title>
		<link>https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/</link>
					<comments>https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 09 May 2026 13:03:00 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3255</guid>

					<description><![CDATA[<p>You&#8217;ve got 85% code coverage. Your CI pipeline is green. You ship to production — and things break in ways your tests never caught. Sound familiar? I&#8217;ve been there. And for a long time, I thought the answer was more tests. What I actually needed was better tests. That&#8217;s exactly what mutation testing taught me, [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/">How Mutation Testing Exposes the Truth (PHP 2026 Edition)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">You&#8217;ve got 85% code coverage. Your CI pipeline is green. You ship to production — and things break in ways your tests never caught. Sound familiar?</p>



<p class="wp-block-paragraph">I&#8217;ve been there. And for a long time, I thought the answer was <em>more</em> tests. What I actually needed was <em>better</em> tests. That&#8217;s exactly what mutation testing taught me, and after using <a href="https://infection.github.io/">Infection PHP</a> in production projects through 2025 and into 2026, I can confidently say it changed how I think about test quality entirely.</p>



<p class="wp-block-paragraph">Previous article in this category: <a href="https://codecraftdiary.com/2026/04/18/laravel-testing-mistakes/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/04/18/laravel-testing-mistakes/</a></p>



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



<h2 class="wp-block-heading">The Dirty Secret of Code Coverage</h2>



<p class="wp-block-paragraph">Code coverage tells you which lines were <em>executed</em> during your test run. It says nothing about whether your assertions are actually meaningful.</p>



<p class="wp-block-paragraph">Consider this classic trap:</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>&lt;?php

class OrderDiscountCalculator
{
    public function calculate(float $price, int $quantity): float
    {
        if ($quantity >= 10) {
            return $price * 0.9;
        }

        return $price;
    }
}
</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">&lt;?php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderDiscountCalculator</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">calculate</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$price</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">float</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: #9CDCFE">$quantity</span><span style="color: #D4D4D4"> &gt;= </span><span style="color: #B5CEA8">10</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">$price</span><span style="color: #D4D4D4"> * </span><span style="color: #B5CEA8">0.9</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: #9CDCFE">$price</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 class="wp-block-paragraph">And a test that covers it 100%:</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>&lt;?php

use PHPUnit\Framework\TestCase;

class OrderDiscountCalculatorTest extends TestCase
{
    public function testCalculate(): void
    {
        $calculator = new OrderDiscountCalculator();

        // Both branches hit — 100% coverage!
        $calculator->calculate(100.0, 15);
        $calculator->calculate(100.0, 5);
    }
}
</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">&lt;?php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> PHPUnit\Framework\</span><span style="color: #4EC9B0">TestCase</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">OrderDiscountCalculatorTest</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">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">testCalculate</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: #9CDCFE">$calculator</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderDiscountCalculator</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Both branches hit — 100% coverage!</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$calculator</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">calculate</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">100.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">15</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$calculator</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">calculate</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">100.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">5</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 class="wp-block-paragraph">This test covers 100% of the code. It also asserts absolutely nothing. If someone changes <code>0.9</code> to <code>0.5</code>, your test suite stays green while your customers get 50% off everything. That&#8217;s a very expensive bug.</p>



<p class="wp-block-paragraph">This is precisely the problem mutation testing solves.</p>



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



<h2 class="wp-block-heading">What Is Mutation Testing?</h2>



<p class="wp-block-paragraph">Mutation testing works by automatically introducing small bugs — called <strong>mutants</strong> — into your source code, then running your test suite against each mutated version. If your tests catch the bug (the mutant is <strong>killed</strong>), great. If your tests still pass with the bug in place (the mutant <strong>survives</strong>), you have a gap.</p>



<p class="wp-block-paragraph">Common mutations include things like:</p>



<ul class="wp-block-list">
<li>Changing <code>&gt;=</code> to <code>&gt;</code> or <code>&lt;=</code></li>



<li>Replacing <code>+</code> with <code>-</code></li>



<li>Flipping <code>true</code> to <code>false</code></li>



<li>Removing entire <code>return</code> statements</li>
</ul>



<p class="wp-block-paragraph">The metric you care about is the <strong>Mutation Score Indicator (MSI)</strong> — the percentage of mutants your tests kill. A high MSI means your tests are genuinely sensitive to regressions.</p>



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



<h2 class="wp-block-heading">Getting Started with Infection PHP</h2>



<p class="wp-block-paragraph"><a href="https://infection.github.io/">Infection</a> is the de facto mutation testing framework for PHP. It integrates cleanly with PHPUnit and runs as a Composer dev dependency.</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>composer require --dev infection/infection
</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">composer </span><span style="color: #C586C0">require</span><span style="color: #D4D4D4"> --dev infection/infection</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 class="wp-block-paragraph">Run it for the first time with:</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>./vendor/bin/infection --threads=4
</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">.</span><span style="color: #D4D4D4">/vendor/bin/infection --threads=</span><span style="color: #B5CEA8">4</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 class="wp-block-paragraph">Infection will run your existing test suite, then start generating and testing mutants. On a modern project with <code>--threads=4</code>, it&#8217;s fast enough to include in a CI pipeline.</p>



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



<h2 class="wp-block-heading">A Real-World Example: Catching What Coverage Misses</h2>



<p class="wp-block-paragraph">Let me walk you through a scenario I actually encountered on a SaaS project — a pricing engine with tiered discounts.</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>&lt;?php

class TieredPricingService
{
    private const TIERS = &#91;
        100 => 0.70, // 30% discount for 100+
        50  => 0.80, // 20% discount for 50+
        10  => 0.90, // 10% discount for 10+
    &#93;;

    public function getPrice(float $unitPrice, int $quantity): float
    {
        foreach (self::TIERS as $minQuantity => $multiplier) {
            if ($quantity >= $minQuantity) {
                return round($unitPrice * $multiplier * $quantity, 2);
            }
        }

        return round($unitPrice * $quantity, 2);
    }
}
</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">&lt;?php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TieredPricingService</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">const</span><span style="color: #D4D4D4"> TIERS = &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #B5CEA8">100</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">0.70</span><span style="color: #D4D4D4">, </span><span style="color: #6A9955">// 30% discount for 100+</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #B5CEA8">50</span><span style="color: #D4D4D4">  =&gt; </span><span style="color: #B5CEA8">0.80</span><span style="color: #D4D4D4">, </span><span style="color: #6A9955">// 20% discount for 50+</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #B5CEA8">10</span><span style="color: #D4D4D4">  =&gt; </span><span style="color: #B5CEA8">0.90</span><span style="color: #D4D4D4">, </span><span style="color: #6A9955">// 10% discount for 10+</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: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">getPrice</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">float</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::TIERS as </span><span style="color: #9CDCFE">$minQuantity</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$multiplier</span><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: #9CDCFE">$quantity</span><span style="color: #D4D4D4"> &gt;= </span><span style="color: #9CDCFE">$minQuantity</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: #DCDCAA">round</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$multiplier</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">2</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: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">round</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">2</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 class="wp-block-paragraph">My original tests covered all branches. PHPUnit reported 100% coverage. But when I ran Infection, it flagged a surviving mutant — it changed <code>&gt;=</code> to <code>&gt;</code> in the tier check, and my test for exactly 10 units didn&#8217;t catch it because I only tested with 11. The boundary condition was untested.</p>



<p class="wp-block-paragraph">Here&#8217;s what the corrected test looked like after Infection exposed the gap:</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>&lt;?php

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class TieredPricingServiceTest extends TestCase
{
    private TieredPricingService $service;

    protected function setUp(): void
    {
        $this->service = new TieredPricingService();
    }

    #&#91;DataProvider('pricingProvider')&#93;
    public function testGetPrice(float $unitPrice, int $quantity, float $expected): void
    {
        $this->assertSame($expected, $this->service->getPrice($unitPrice, $quantity));
    }

    public static function pricingProvider(): array
    {
        return [
            'below first tier'         => &#91;10.0, 5,   50.00&#93;,
            'exactly at 10 tier'       => &#91;10.0, 10,  90.00&#93;,  // boundary — was missing!
            'above 10 tier'            => &#91;10.0, 11,  99.00&#93;,
            'exactly at 50 tier'       => &#91;10.0, 50,  400.00&#93;, // boundary
            'exactly at 100 tier'      => &#91;10.0, 100, 700.00&#93;, // boundary
            'above highest tier'       => &#91;10.0, 200, 1400.00&#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: #D4D4D4">&lt;?php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> PHPUnit\Framework\</span><span style="color: #4EC9B0">TestCase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> PHPUnit\Framework\Attributes\</span><span style="color: #4EC9B0">DataProvider</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">TieredPricingServiceTest</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">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TieredPricingService</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">protected</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">setUp</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">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">service</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TieredPricingService</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">    #&#91;DataProvider(</span><span style="color: #CE9178">&#39;pricingProvider&#39;</span><span style="color: #D4D4D4">)&#93;</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">testGetPrice</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$expected</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">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertSame</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$expected</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">getPrice</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$quantity</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">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">pricingProvider</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>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;below first tier&#39;</span><span style="color: #D4D4D4">         =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">,   </span><span style="color: #B5CEA8">50.00</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;exactly at 10 tier&#39;</span><span style="color: #D4D4D4">       =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">10</span><span style="color: #D4D4D4">,  </span><span style="color: #B5CEA8">90.00</span><span style="color: #D4D4D4">&#93;,  </span><span style="color: #6A9955">// boundary — was missing!</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;above 10 tier&#39;</span><span style="color: #D4D4D4">            =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">11</span><span style="color: #D4D4D4">,  </span><span style="color: #B5CEA8">99.00</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;exactly at 50 tier&#39;</span><span style="color: #D4D4D4">       =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">50</span><span style="color: #D4D4D4">,  </span><span style="color: #B5CEA8">400.00</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #6A9955">// boundary</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;exactly at 100 tier&#39;</span><span style="color: #D4D4D4">      =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">100</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">700.00</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #6A9955">// boundary</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;above highest tier&#39;</span><span style="color: #D4D4D4">       =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">200</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">1400.00</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 class="wp-block-paragraph">After adding boundary assertions, Infection&#8217;s MSI jumped from 61% to 94%. That&#8217;s the difference between a test suite that gives you false confidence and one that actually has your back.</p>



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



<h2 class="wp-block-heading">Configuring Infection for Your Project</h2>



<p class="wp-block-paragraph">Infection is configured via <code>infection.json5</code> in your project root. Here&#8217;s a production-ready config I use:</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>{
    "$schema": "vendor/infection/infection/resources/schema.json",
    "source": {
        "directories": &#91;"src"&#93;,
        "excludes": &#91;"src/Infrastructure/Migrations"&#93;
    },
    "mutators": {
        "@default": true
    },
    "testFramework": "phpunit",
    "testFrameworkOptions": "--testsuite=unit",
    "minMsi": 85,
    "minCoveredMsi": 90,
    "threads": 4,
    "logs": {
        "text": "var/log/infection.log",
        "html": "var/log/infection.html",
        "summary": "var/log/infection-summary.log"
    }
}
</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">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;</span><span style="color: #9CDCFE">$schema</span><span style="color: #CE9178">&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;vendor/infection/infection/resources/schema.json&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;source&quot;</span><span style="color: #D4D4D4">: {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;directories&quot;</span><span style="color: #D4D4D4">: &#91;</span><span style="color: #CE9178">&quot;src&quot;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;excludes&quot;</span><span style="color: #D4D4D4">: &#91;</span><span style="color: #CE9178">&quot;src/Infrastructure/Migrations&quot;</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 style="color: #CE9178">&quot;mutators&quot;</span><span style="color: #D4D4D4">: {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;@default&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 style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;testFramework&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;phpunit&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;testFrameworkOptions&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;--testsuite=unit&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;minMsi&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">85</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;minCoveredMsi&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">90</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;threads&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">4</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;logs&quot;</span><span style="color: #D4D4D4">: {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;text&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;var/log/infection.log&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;html&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;var/log/infection.html&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;summary&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;var/log/infection-summary.log&quot;</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 class="wp-block-paragraph">The <code>minMsi</code> and <code>minCoveredMsi</code> thresholds are important — they let your CI pipeline fail if mutation score drops below acceptable levels, the same way PHPUnit can fail below a coverage threshold.</p>



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



<h2 class="wp-block-heading">Integrating Into CI (GitHub Actions)</h2>



<p class="wp-block-paragraph">Here&#8217;s a GitHub Actions job I&#8217;ve been running since mid-2025:</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>mutation-testing:
  runs-on: ubuntu-latest
  needs: tests
  steps:
    - uses: actions/checkout@v4

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.5'
        coverage: xdebug

    - name: Install dependencies
      run: composer install --no-interaction

    - name: Run Infection
      run: ./vendor/bin/infection --threads=4 --min-msi=85 --min-covered-msi=90
</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">mutation-testing:</span></span>
<span class="line"><span style="color: #D4D4D4">  runs-</span><span style="color: #569CD6">on</span><span style="color: #D4D4D4">: ubuntu-latest</span></span>
<span class="line"><span style="color: #D4D4D4">  needs: tests</span></span>
<span class="line"><span style="color: #D4D4D4">  steps:</span></span>
<span class="line"><span style="color: #D4D4D4">    - uses: actions/checkout@v4</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    - name: Setup PHP</span></span>
<span class="line"><span style="color: #D4D4D4">      uses: shivammathur/setup-php@v2</span></span>
<span class="line"><span style="color: #D4D4D4">      with:</span></span>
<span class="line"><span style="color: #D4D4D4">        php-version: </span><span style="color: #CE9178">&#39;8.5&#39;</span></span>
<span class="line"><span style="color: #D4D4D4">        coverage: xdebug</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    - name: Install dependencies</span></span>
<span class="line"><span style="color: #D4D4D4">      run: composer install --</span><span style="color: #569CD6">no</span><span style="color: #D4D4D4">-interaction</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    - name: Run Infection</span></span>
<span class="line"><span style="color: #D4D4D4">      run: </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4">/vendor/bin/infection --threads=</span><span style="color: #B5CEA8">4</span><span style="color: #D4D4D4"> --min-msi=</span><span style="color: #B5CEA8">85</span><span style="color: #D4D4D4"> --min-covered-msi=</span><span style="color: #B5CEA8">90</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 class="wp-block-paragraph">One important note: Infection requires a coverage driver (Xdebug or PCOV) to know which mutants are relevant to which tests. PCOV is faster for large codebases; Xdebug gives more detail. I use Xdebug locally and PCOV in CI.</p>



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



<h2 class="wp-block-heading">Common Objections — And Honest Answers</h2>



<p class="wp-block-paragraph"><strong>&#8220;It&#8217;s too slow.&#8221;</strong> It can be on large codebases, but <code>--threads</code> and configuring <code>source.excludes</code> to skip generated code, migrations, and DTOs makes a huge difference. I typically exclude everything that has no business logic.</p>



<p class="wp-block-paragraph"><strong>&#8220;The MSI is too low to be useful.&#8221;</strong> Start with <code>--min-msi=0</code> and just look at the HTML report. Prioritize killing mutants in your core domain logic first — that&#8217;s where bugs actually hurt.</p>



<p class="wp-block-paragraph"><strong>&#8220;It produces too many surviving mutants.&#8221;</strong> Some mutants are genuinely equivalent (they don&#8217;t change behavior). Infection lets you mark these as ignored in config. Over time your noise floor drops significantly.</p>



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



<h2 class="wp-block-heading">What My Workflow Looks Like in 2026</h2>



<p class="wp-block-paragraph">My current approach on active PHP projects:</p>



<ol class="wp-block-list">
<li><strong>PHPUnit with strict coverage</strong> for the fast feedback loop during development.</li>



<li><strong>Infection on every PR</strong> targeting only changed files — using <code>--git-diff-filter</code> (available since Infection 0.27) to keep CI times reasonable.</li>



<li><strong>Full Infection run weekly</strong> on the <code>main</code> branch to catch gradual MSI drift.</li>
</ol>



<p class="wp-block-paragraph">The <code>--git-diff-filter</code> flag is a game-changer for larger repos — it only mutates code touched in the current diff, so mutation testing stays practical even on monorepos.</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>./vendor/bin/infection --git-diff-filter=AM --threads=8
</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">.</span><span style="color: #D4D4D4">/vendor/bin/infection --git-diff-filter=AM --threads=</span><span style="color: #B5CEA8">8</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>



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



<h2 class="wp-block-heading">Final Thoughts</h2>



<p class="wp-block-paragraph">Code coverage is a floor, not a ceiling. It tells you the minimum — which lines were touched. Mutation testing tells you something far more valuable: whether those lines are <em>protected</em> by tests that would actually catch a regression.</p>



<p class="wp-block-paragraph">If you&#8217;re publishing technical content in 2026 and you&#8217;re not talking about mutation testing, you&#8217;re leaving one of PHP&#8217;s most powerful quality tools completely off the table. The tooling has matured, the CI integration is straightforward, and the payoff in confidence is real.</p>



<p class="wp-block-paragraph">Start with a single service class. Run Infection. Look at what survives. I promise you&#8217;ll find something surprising.</p>



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



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



<p class="wp-block-paragraph"><strong>Resources:</strong></p>



<ul class="wp-block-list">
<li><a href="https://infection.github.io/guide/">Infection PHP documentation</a></li>



<li><a href="https://github.com/infection/infection">Infection GitHub repository</a></li>



<li><a href="https://phpunit.de/">PHPUnit documentation</a></li>
</ul>
<p>The post <a href="https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/">How Mutation Testing Exposes the Truth (PHP 2026 Edition)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
