<?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>programming Archives - CodeCraft Diary</title>
	<atom:link href="https://codecraftdiary.com/tag/programming/feed/" rel="self" type="application/rss+xml" />
	<link>https://codecraftdiary.com/tag/programming/</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>programming Archives - CodeCraft Diary</title>
	<link>https://codecraftdiary.com/tag/programming/</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>
	</channel>
</rss>
