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

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

<image>
	<url>https://codecraftdiary.com/wp-content/uploads/2025/10/cropped-IMG_3463-32x32.png</url>
	<title>codecraftdiary, Author at CodeCraft Diary</title>
	<link>https://codecraftdiary.com/author/jwchildroot/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Refactoring the Database Layer: Replacing Query Scope Bloat with Repository/Query Objects</title>
		<link>https://codecraftdiary.com/2026/08/30/refactoring-the-database-layer-replacing-query-scope-bloat-with-repository-query-objects/</link>
					<comments>https://codecraftdiary.com/2026/08/30/refactoring-the-database-layer-replacing-query-scope-bloat-with-repository-query-objects/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 30 Aug 2026 13:15:00 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3359</guid>

					<description><![CDATA[<p>Every developer who has maintained a Laravel application past its second birthday knows the exact sequence of events. You start with a clean User or Order model. It handles basic authentication, a couple of relationships, and maybe a simple active status scope. It’s elegant, intuitive, and feels like the framework working for you. Fast forward [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/08/30/refactoring-the-database-layer-replacing-query-scope-bloat-with-repository-query-objects/">Refactoring the Database Layer: Replacing Query Scope Bloat with Repository/Query Objects</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Every developer who has maintained a Laravel application past its second birthday knows the exact sequence of events. You start with a clean <code>User</code> or <code>Order</code> model. It handles basic authentication, a couple of relationships, and maybe a simple active status scope. It’s elegant, intuitive, and feels like the framework working <em>for</em> you.</p>



<p class="wp-block-paragraph">Fast forward eighteen months. That same <code>Order</code> Eloquent model is now a 1,400-line monolith. Nestled between event listeners, accessor mutations, and custom attribute cast methods sit dozens of local query scopes: <code>scopeWhereActive()</code>, <code>scopeWhereEligibleForDiscount()</code>, <code>scopePendingExport()</code>, <code>scopeWithRecentTransactions()</code>.</p>



<p class="wp-block-paragraph">We call this <strong>Query Scope Bloat</strong>. On paper, query scopes are advertised as the standard Laravel way to encapsulate reusable query logic. In practice, when applied indiscriminately to complex domain models, they turn your database interactions into an unmaintainable, tightly coupled mess.</p>



<p class="wp-block-paragraph">In this post, we’re going to walk through why query scopes break down at scale and how to refactor them into crisp, single-purpose <strong>Query Objects</strong> (and where traditional Repositories fit into the picture).</p>



<p class="wp-block-paragraph">Last article in this category: <a href="https://codecraftdiary.com/2026/07/31/laravel-models-business-logic-hidden-workflows/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/07/31/laravel-models-business-logic-hidden-workflows/</a></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-6">The Fallacy of the &#8220;Convenient&#8221; Model Scope</h2>



<p class="wp-block-paragraph">Let&#8217;s look at how query scope bloat sneaks into a codebase. Imagine an e-commerce platform where marketing wants to identify high-value B2B customers who haven&#8217;t placed an order in the last 90 days but have active subscriptions.</p>



<p class="wp-block-paragraph">The path of least resistance is usually adding a scope directly to the <code>Customer</code> model:</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>// App\Models\Customer.php

class Customer extends Model
{
    public function scopeDormantHighValueB2b($query, int $daysThreshold = 90)
    {
        return $query->where('type', 'b2b')
            ->whereHas('subscriptions', function ($q) {
                $q->where('status', 'active');
            })
            ->whereDoesntHave('orders', function ($q) use ($daysThreshold) {
                $q->where('created_at', '>=', now()->subDays($daysThreshold));
            })
            ->having('total_lifetime_spend', '>', 10000);
    }
}
</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\Models\Customer.php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Customer</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Model</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">scopeDormantHighValueB2b</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$daysThreshold</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">90</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><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;b2b&#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;subscriptions&#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;active&#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">whereDoesntHave</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 style="color: #569CD6">use</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$daysThreshold</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;created_at&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;&gt;=&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">subDays</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$daysThreshold</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">having</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;total_lifetime_spend&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;&gt;&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">10000</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">It works. It&#8217;s expressive enough in the controller: <code>Customer::dormantHighValueB2b()-&gt;get()</code>. But consider the structural consequences over time:</p>



<ol start="1" class="wp-block-list">
<li><strong>Violation of Single Responsibility Principle (SRP):</strong> The <code>Customer</code> model is now responsible for database mapping, attribute casting, relationship definitions, business rules, AND specific reporting analytics filters.</li>



<li><strong>Unintended Side Effects &amp; Hidden Joins:</strong> Scopes often chain sub-queries or eager loads. When a developer chains three different scopes together in a controller, they frequently create accidental <code>N+1</code> query patterns or conflicting <code>WHERE</code> clauses that silently override each other.</li>



<li><strong>Zero Encapsulation for Complex Returns:</strong> Scopes return an <code>Illuminate\Database\Eloquent\Builder</code> instance. This means any caller can keep chaining arbitrary SQL onto the end of your query, leaking implementation details across controller actions or job handlers.</li>



<li><strong>Test Rigidity:</strong> To test this single query rule, you must instantiate the entire Eloquent model, set up database state for related subscriptions and orders, and run full integration tests every single time.</li>
</ol>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-7">Enter Query Objects</h2>



<p class="wp-block-paragraph">Before jumping straight to a heavy Repository pattern interface with 20 methods, there is a much cleaner, more idiomatic solution for complex read queries: <strong>Query Objects</strong>.</p>



<p class="wp-block-paragraph">A Query Object is a dedicated, invokable class whose sole job is to construct and execute a single database query. It isolates query construction from your domain models and controllers, making your database layer modular and trivially testable.</p>



<h3 class="wp-block-heading">Step 1: Extracting the Query to a Dedicated Object</h3>



<p class="wp-block-paragraph">Let&#8217;s refactor our dormant B2B customer query into an invokable Query Object:</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\Queries;

use App\Models\Customer;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;

final class GetDormantHighValueB2bCustomersQuery
{
    public function __construct(
        private readonly int $daysThreshold = 90,
        private readonly int $minimumSpend = 10000
    ) {}

    public function handle(int $perPage = 25): LengthAwarePaginator
    {
        return $this->getBaseQuery()
            ->paginate($perPage);
    }

    public function getBaseQuery(): Builder
    {
        return Customer::query()
            ->where('type', 'b2b')
            ->whereHas('subscriptions', fn (Builder $q) => $q->where('status', 'active'))
            ->whereDoesntHave('orders', fn (Builder $q) => 
                $q->where('created_at', '>=', now()->subDays($this->daysThreshold))
            )
            ->where('total_lifetime_spend', '>=', $this->minimumSpend);
    }
}
</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\Queries</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">Customer</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Contracts\Pagination\</span><span style="color: #4EC9B0">LengthAwarePaginator</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">final</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">GetDormantHighValueB2bCustomersQuery</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$daysThreshold</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">90</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$minimumSpend</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">10000</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$perPage</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">25</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">LengthAwarePaginator</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">getBaseQuery</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">paginate</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$perPage</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">getBaseQuery</span><span style="color: #D4D4D4">(): </span><span style="color: #4EC9B0">Builder</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Customer</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;type&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;b2b&#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;subscriptions&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">fn</span><span style="color: #D4D4D4"> (</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) =&gt; </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;active&#39;</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;orders&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">fn</span><span style="color: #D4D4D4"> (</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) =&gt; </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;created_at&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;&gt;=&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">subDays</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">daysThreshold</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">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;total_lifetime_spend&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;&gt;=&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">minimumSpend</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">Notice what changed here:</p>



<ul class="wp-block-list">
<li>The query constraints live in an explicit file inside <code>App\Queries</code>.</li>



<li>Parameters like threshold days or minimum spend are strongly typed in the constructor.</li>



<li>The model class remains lightweight, focusing purely on schema definition and relationships.</li>
</ul>



<h3 class="wp-block-heading">Step 2: Consuming Query Objects in Action Classes or Controllers</h3>



<p class="wp-block-paragraph">Now, inside your controller, job, or CLI command, you simply inject or instantiate the Query Object:</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\Queries\GetDormantHighValueB2bCustomersQuery;
use Illuminate\Http\JsonResponse;

class MarketingTargetController extends Controller
{
    public function index(): JsonResponse
    {
        $query = new GetDormantHighValueB2bCustomersQuery(
            daysThreshold: 60,
            minimumSpend: 15000
        );

        $customers = $query->handle(perPage: 50);

        return response()->json($customers);
    }
}
</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\Queries\</span><span style="color: #4EC9B0">GetDormantHighValueB2bCustomersQuery</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">JsonResponse</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">MarketingTargetController</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">index</span><span style="color: #D4D4D4">(): </span><span style="color: #4EC9B0">JsonResponse</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"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">GetDormantHighValueB2bCustomersQuery</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            daysThreshold: </span><span style="color: #B5CEA8">60</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            minimumSpend: </span><span style="color: #B5CEA8">15000</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$customers</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">perPage</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">50</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$customers</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-8">Repositories vs. Query Objects: Clearing the Confusion</h2>



<p class="wp-block-paragraph">In the PHP community, the &#8220;Repository Pattern&#8221; was heavily pushed years ago as the ultimate solution to Eloquent coupling. Many developers created interface-heavy structures like <code>CustomerRepositoryInterface</code> with methods like <code>find()</code>, <code>save()</code>, <code>getDormant()</code>, <code>getRecent()</code>, <code>getVip()</code>.</p>



<p class="wp-block-paragraph">In 90% of Laravel projects, generic repositories turn into <strong>God Interfaces</strong>. They become a dump for every custom query the app will ever run.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Architectural Rule of Thumb:</strong></p>



<p class="wp-block-paragraph"><strong>Repositories</strong> should be used for <em>write operations</em> or domain-entity hydration where you need to completely decouple from ORM storage details.</p>



<p class="wp-block-paragraph"><strong>Query Objects</strong> should be used for <em>read operations</em>, complex filtering, reports, and search queries.</p>
</blockquote>



<p class="wp-block-paragraph">By separating reads into Query Objects, you respect CQRS (Command Query Responsibility Segregation) light principles without adding overwhelming boilerplate.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-9">When SHOULD You Still Use Local Query Scopes?</h2>



<p class="wp-block-paragraph">Refactoring doesn&#8217;t mean deleting every single query scope from your application. Scopes are still fantastic for <strong>atomic, single-column boolean conditions</strong> that are ubiquitous across the domain.</p>



<p class="wp-block-paragraph">Good candidates for query scopes:</p>



<ul class="wp-block-list">
<li><code>scopePublished($query)</code> -> <code>$query->whereNotNull('published_at')</code></li>



<li><code>scopeWhereActive($query)</code> -> <code>$query->where('is_active', true)</code></li>
</ul>



<p class="wp-block-paragraph"><strong>Rule of thumb:</strong> If a scope requires joining multiple tables, complex grouping, or specific business analytics logic, <strong>pull it out into a Query Object immediately</strong>.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-10">Key Takeaways for Your Codebase Today</h2>



<ol start="1" class="wp-block-list">
<li><strong>Keep Models Lean:</strong> Your Eloquent models should describe data structure, relationships, and basic mutations—not multi-table analytical queries.</li>



<li><strong>One Query, One Class:</strong> When a query spans multiple relationships or complex conditionals, create a dedicated class under <code>App\Queries</code>.</li>



<li><strong>Avoid Mega-Repositories:</strong> Don&#8217;t trade Fat Models for Fat Repositories. Use invokable, single-purpose Query Objects for complex reads.</li>



<li><strong>Test in Isolation:</strong> Query Objects allow you to test complex query constraints with dedicated database factories without clogging your primary model test suites.</li>
</ol>



<p class="wp-block-paragraph">Tidying up your database layer isn&#8217;t about dogmatic architectural purity—it&#8217;s about making sure that six months from now, when marketing changes the criteria for &#8220;dormant customers,&#8221; you only have to edit a single 30-line file without breaking three other features.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/08/30/refactoring-the-database-layer-replacing-query-scope-bloat-with-repository-query-objects/">Refactoring the Database Layer: Replacing Query Scope Bloat with Repository/Query Objects</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/08/30/refactoring-the-database-layer-replacing-query-scope-bloat-with-repository-query-objects/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Staging Environments Are a Deployment Problem, Not an Infrastructure Problem</title>
		<link>https://codecraftdiary.com/2026/08/23/staging-environments-deployment-problem/</link>
					<comments>https://codecraftdiary.com/2026/08/23/staging-environments-deployment-problem/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 23 Aug 2026 13:18:00 +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=3347</guid>

					<description><![CDATA[<p>This isn&#8217;t an argument against testing before production. It&#8217;s an argument against treating a long-lived shared staging environment as your primary safety mechanism. Every software engineer has lived through this exact Thursday afternoon nightmare: The feature was thoroughly tested on the testing server. The CI/CD pipeline was a sea of reassuring green checkmarks. QA gave [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/08/23/staging-environments-deployment-problem/">Staging Environments Are a Deployment Problem, Not an Infrastructure Problem</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">This isn&#8217;t an argument against testing before production. It&#8217;s an argument against treating a long-lived shared staging environment as your primary safety mechanism.</p>



<p class="wp-block-paragraph">Every software engineer has lived through this exact Thursday afternoon nightmare: The feature was thoroughly tested on the testing server. The CI/CD pipeline was a sea of reassuring green checkmarks. QA gave their final stamp of approval. With absolute confidence, you clicked &#8220;Deploy to Production.&#8221;</p>



<p class="wp-block-paragraph">Ten minutes later, the alert channels started screaming, database connections saturated, and customers began reporting broken checkout flows.</p>



<p class="wp-block-paragraph">When the post-mortem rolled around, the consensus was predictable: <em>&#8220;The testing environment diverged from Production again. We need to sync the databases more often. We need to mirror the production setup exactly.&#8221;</em></p>



<p class="wp-block-paragraph">So, the team spends two sprints setting up complex data-anonymization pipelines and mirroring server configurations. Yet, three months later, the exact same production incident happens again.</p>



<p class="wp-block-paragraph">After few years of working on backend systems, debugging production outages, and trying to keep pipelines fast, I&#8217;ve come to a realization that many engineering teams refuse to accept: <strong>staging environments are lying to you—or, more accurately, long-lived shared staging environments often give teams a false sense of confidence.</strong></p>



<p class="wp-block-paragraph">Your testing setup isn&#8217;t failing because your DevOps team is lazy or because you haven&#8217;t automated your database dumps. It is failing because treating a shared testing environment as a safety net is an architectural and process flaw. Staging environments are a deployment problem, not an infrastructure problem.</p>



<p class="wp-block-paragraph">Here you can find last article of this category: <a href="https://codecraftdiary.com/2026/07/26/merge-conflicts-process-problem/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/07/26/merge-conflicts-process-problem/</a></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-19">The Four Myths of the &#8220;Identical&#8221; Staging Environment</h2>



<p class="wp-block-paragraph">We keep clinging to the idea of a pre-production testing environment because it feels safe. It gives us a comfortable illusion of risk mitigation. But when you look at how modern backend systems operate, the concept of a true &#8220;pre-production&#8221; environment completely falls apart for four main reasons.</p>



<h3 class="wp-block-heading">1. The Data Drift Reality</h3>



<p class="wp-block-paragraph">You can dump your production database, run anonymization scripts, and seed your testing server every single night. It still won&#8217;t matter. Production data is dynamic, unpredictable, and dirty. It contains edge-case state combinations created by real users over years of operating the system.</p>



<p class="wp-block-paragraph">A staging environment usually contains pristine, synthetic test data or a stripped-down database dump. You are unlikely to catch a bug that only triggers when a user has a legacy account created in 2021, three active subscriptions, and a special character in their billing address—because that specific state combination simply doesn&#8217;t exist outside of Production.</p>



<h3 class="wp-block-heading">2. Third-Party Sandboxes Are Half-Baked</h3>



<p class="wp-block-paragraph">Modern backend architectures rely heavily on third-party APIs: payment processors like Stripe, email providers, or external auth services.</p>



<p class="wp-block-paragraph">The sandbox environments provided by these services rarely behave like their production counterparts. A test mode won&#8217;t simulate exact rate limits, network latency, or edge-case webhook delivery failures. When your staging setup relies on mock services or sandbox APIs, you aren&#8217;t testing your application against reality; you are testing it against a simplified toy version of reality.</p>



<h3 class="wp-block-heading">3. Traffic and Concurrency Don&#8217;t Exist on Testing Servers</h3>



<p class="wp-block-paragraph">Unless you are running expensive, continuous load testing against your pre-production environment (which almost nobody actually does), your testing server is dead quiet.</p>



<p class="wp-block-paragraph">Your service running on Staging handles one or two requests at a time while a developer or QA engineer manually clicks through a flow. In Production, that same service handles thousands of concurrent requests, competing for thread pools, database connections, and cache space. Race conditions, deadlocks, and connection pool exhaustion only happen under real load. A staging server is fundamentally blind to them.</p>



<h3 class="wp-block-heading">4. The Maintenance Sinkhole</h3>



<p class="wp-block-paragraph">Maintaining a testing server that somewhat resembles Production takes an immense amount of engineering effort. Databases need maintenance, API keys expire, test accounts get corrupted, and environment variables drift.</p>



<p class="wp-block-paragraph">I’ve seen senior engineers spend 20% of their sprint time debugging issues that turned out to be &#8220;just a test server configuration issue.&#8221; That is wasted engineering effort spent maintaining an environment that doesn&#8217;t generate a single dollar of business value.</p>



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



<p class="has-vivid-cyan-blue-color has-text-color has-link-color has-large-font-size wp-elements-20 wp-block-paragraph">What Staging Is Actually Good For</p>



<p class="wp-block-paragraph">None of this means staging environments are useless.</p>



<p class="wp-block-paragraph">They still provide value for infrastructure validation, smoke testing after deployments, manual QA of integrated systems, customer demonstrations, and validating deployment pipelines.</p>



<p class="wp-block-paragraph">The mistake isn&#8217;t having a staging environment. The mistake is treating it as the ultimate source of confidence before Production. A shared staging server should support your deployment process—not become the gatekeeper that every change must pass through.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-21">How Staging Bottlenecks Your Entire Workflow</h2>



<p class="wp-block-paragraph">The technical limitations are bad enough, but the cultural impact on your team is worse.</p>



<p class="wp-block-paragraph">When a team relies on a single shared testing server, it becomes a bottleneck for the entire delivery process. Developer A finishes a feature and deploys it to the testing server. Developer B finishes a completely unrelated bug fix and also deploys it there. Now, QA starts testing Developer B’s fix, but something breaks because Developer A’s feature introduced a bug.</p>



<p class="wp-block-paragraph">Suddenly, nobody knows whose code broke the build. Deployment to Production gets blocked for everyone. What started as two small, independent changes transforms into a massive, tangled deployment batch.</p>



<p class="wp-block-paragraph">By trying to make deployment &#8220;safer&#8221; through a shared testing environment, teams inadvertently recreate the exact problem they tried to avoid: <strong>large, high-risk releases.</strong></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-22">Shift Your Mindset: From Pre-Production Safety to Production Resilience</h2>



<p class="wp-block-paragraph">If we accept that a testing server can never accurately predict how code will behave in Production, we have to change our approach. The goal shouldn&#8217;t be <em>preventing all bugs from reaching Production through pre-production testing</em>. The goal should be <strong>minimizing the blast radius of changes and recovering instantly when things go wrong.</strong></p>



<p class="wp-block-paragraph">High-performing engineering organizations don&#8217;t rely on long-lived testing servers. They shift their focus toward modern deployment practices.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-23">Ephemeral (Preview) Environments</h3>



<p class="wp-block-paragraph">If you need an environment to test a feature before merging, don&#8217;t use a shared long-lived test server. Use ephemeral environments.</p>



<p class="wp-block-paragraph">Preview environments solve a completely different problem than staging: they validate isolated changes rather than simulating production.</p>



<p class="wp-block-paragraph">Whenever a developer opens a Pull Request, your CI pipeline can spin up a temporary, isolated Docker container or preview environment containing only that branch&#8217;s changes. The developer or QA can test the specific feature, run integration tests, and once the PR is merged or closed, the environment automatically destroys itself. This completely eliminates queueing, environment pollution, and cross-feature interference.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-24">Progressive Delivery and Feature Flags</h3>



<p class="wp-block-paragraph">Instead of holding code back on a testing server for weeks, deploy code to Production immediately—behind a feature flag.</p>



<p class="wp-block-paragraph">Feature flags decouple <em>deployment</em> (moving code to servers) from <em>release</em> (making functionality visible to users). You can deploy a new feature to Production on Monday, test it internally in the live environment using your own staff accounts, turn it on for 1% of real users on Wednesday, and gradually scale it to 100% by Friday. If something breaks at 1%, the blast radius is tiny, and turning off the flag takes milliseconds.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-25">Observability and Fast Rollbacks</h3>



<p class="wp-block-paragraph">Your time is much better spent improving your production observability than fixing a testing server. Invest in proper tracing, structured logging, and automated metric alerts.</p>



<p class="wp-block-paragraph">When you pair strong observability with an automated rollback strategy, catching a micro-bug in Production within 60 seconds of deployment is significantly cheaper and safer than spending three weeks manually validating everything on a test server.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-26">Stop Servicing the Mirage</h2>



<p class="wp-block-paragraph">The next time a release breaks in Production, resist the urge to say, <em>&#8220;We need to fix our testing environment.&#8221;</em></p>



<p class="wp-block-paragraph">Step back and look at your workflow. Ask yourself why your Pull Requests are sitting on a testing server for days. Ask why your deployment process requires a green signal from an environment that doesn&#8217;t process real traffic or real data.</p>



<p class="wp-block-paragraph">Staging environments give us a warm, comfortable feeling of safety, but in practice, they act as a tax on developer productivity and a false security blanket. Shrink your releases, flag your features, test in Production safely, and focus your energy on building resilient systems that recover quickly. Your pipeline—and your team—will thank you. The safest deployment process isn&#8217;t the one that spends the most time on staging. It&#8217;s the one that makes every production change small, observable, and reversible.</p>



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



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/08/23/staging-environments-deployment-problem/">Staging Environments Are a Deployment Problem, Not an Infrastructure Problem</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/08/23/staging-environments-deployment-problem/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The Most Common Test Smells in PHP (And How to Fix Them)</title>
		<link>https://codecraftdiary.com/2026/08/09/the-most-common-test-smells-in-php-and-how-to-fix-them/</link>
					<comments>https://codecraftdiary.com/2026/08/09/the-most-common-test-smells-in-php-and-how-to-fix-them/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 09 Aug 2026 13:02:00 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3335</guid>

					<description><![CDATA[<p>Your test suite passes. CI is green. Everyone merges their pull requests with confidence. Then a seemingly harmless refactor breaks production. If that sounds familiar, the problem isn&#8217;t always a lack of tests. Sometimes it&#8217;s the quality of those tests. Just like production code can suffer from code smells, tests can accumulate test smells—patterns that [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/08/09/the-most-common-test-smells-in-php-and-how-to-fix-them/">The Most Common Test Smells in PHP (And How to Fix Them)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Your test suite passes.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">CI is green.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Everyone merges their pull requests with confidence.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Then a seemingly harmless refactor breaks production.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">If that sounds familiar, the problem isn&#8217;t always a lack of tests. Sometimes it&#8217;s the quality of those tests.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Just like production code can suffer from code smells, tests can accumulate <strong>test smells</strong>—patterns that make tests harder to understand, slower to maintain, or less effective at catching real bugs. A passing test suite isn&#8217;t automatically a trustworthy one.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">After reviewing countless PHP projects over the years, I&#8217;ve noticed the same patterns appearing again and again. Whether you&#8217;re using PHPUnit, Pest, Laravel, Symfony, or another PHP framework, these smells are surprisingly common.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Let&#8217;s look at the ones worth eliminating first.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Last article in this Testing category you can find here: <a href="https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-38">1. Assertion Roulette</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">One of the easiest ways to make a test frustrating is filling it with dozens of unrelated assertions.</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_profile()
{
    $user = User::factory()->create();

    $this->assertTrue($user->active);
    $this->assertEquals('John', $user->first_name);
    $this->assertEquals('Doe', $user->last_name);
    $this->assertNotNull($user->email_verified_at);
    $this->assertEquals('admin', $user->role);
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">When this test fails, the failure message rarely tells the whole story. Another developer has to inspect multiple assertions before understanding what actually broke.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">A better approach is giving each test a single responsibility.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Instead of verifying five unrelated things, split them into smaller tests with descriptive names.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Small tests are easier to understand, easier to debug, and much harder to accidentally break.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>Run on OnlinePHP.io</strong></p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>Try It Yourself</strong>: <a href="https://onlinephp.io/c/927b9" target="_blank" rel="noreferrer noopener">https://onlinephp.io/c/927b9</a></p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"></p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-39">2. Mystery Guest</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">A test should be completely understandable on its own.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Unfortunately, many tests quietly depend on hidden data.</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>$user = User::find(1);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Where did user ID 1 come from?</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Was it seeded?</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Created by another test?</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Imported from a dump?</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Nobody knows.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">This hidden dependency is known as the <strong>Mystery Guest</strong> smell.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Modern PHP testing tools make this easy to avoid.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Instead of relying on unknown database state, create exactly what your test needs.</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>$user = User::factory()->create();
</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Now every developer—and every CI pipeline—knows exactly where that user came from.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>Run on OnlinePHP.io</strong></p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>Try It Yourself:</strong><a href="https://onlinephp.io/c/42b4d" target="_blank" rel="noreferrer noopener">https://onlinephp.io/c/42b4d</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-40">3. Fragile Assertions</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Many tests verify far more than they actually need.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Imagine testing an API response.</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>$this->assertEquals(
    $expectedJson,
    $response->getContent()
);
</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Everything looks fine until another developer adds a harmless field like <code>avatar_url</code>.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Nothing is broken.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Your test still fails.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">That&#8217;s not a useful failure—it&#8217;s unnecessary maintenance.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Instead, assert the behavior that actually matters.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">For example, in Laravel:</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>$response->assertJsonFragment(&#091;
    'email' => 'john@example.com',
&#093;);
</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Your tests should protect business behavior, not formatting details that change every sprint.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>Run on OnlinePHP.io</strong></p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>Try It Yourself:</strong> <a href="https://onlinephp.io/c/78fae" target="_blank" rel="noreferrer noopener">https://onlinephp.io/c/78fae</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-41">4. Duplicate Setup Everywhere</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Open a large test suite and sometimes every test starts with exactly the same setup.</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>$user = User::factory()->create();
$account = Account::factory()->create();
$order = Order::factory()->create();
</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Repeat that fifty times and your tests become noisy.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">When setup dominates the test, readers spend more time understanding the preparation than the actual behavior being tested.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Extract common setup into helper methods, builders, or reusable factory states.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">The goal isn&#8217;t fewer lines of code.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">The goal is making the important part of the test immediately obvious.</p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-42">5. Testing Implementation Instead of Behavior</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">This is probably the most expensive mistake teams make.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Imagine changing an internal algorithm.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">The application still behaves exactly the same.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Users notice nothing.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Yet twenty tests suddenly fail.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Why?</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Because they were testing implementation details rather than observable behavior.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">A good test should answer:</p>



<blockquote class="wp-block-quote has-d-4-d-4-d-4-color has-text-color is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;What should the application do?&#8221;</p>
</blockquote>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Not:</p>



<blockquote class="wp-block-quote has-d-4-d-4-d-4-color has-text-color is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;How is the application currently doing it?&#8221;</p>
</blockquote>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Implementation changes happen constantly.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Business behavior should remain stable.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">The closer your tests are to user-visible behavior, the more valuable they&#8217;ll become during refactoring.</p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-43">6. Hardcoded IDs and Magic Values</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Hardcoded values have a habit of surviving far longer than they should.</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>$user = User::find(42);

$this->assertEquals(5, $invoice->status);
</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Six months later, someone changes the seed data.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Now the test mysteriously fails.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Even worse, nobody remembers why the number <strong>42</strong> was special in the first place.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Prefer meaningful constants, enums, or freshly created models.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Your future self will thank you.</p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-44">7. Logic Inside Tests</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Tests should explain behavior.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">They shouldn&#8217;t contain business logic themselves.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">For example:</p>



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

$this->assertEquals($expected, $discount);
</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Now your test contains conditional logic that needs to be understood before anyone can even interpret the assertion.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Instead, create separate test cases.</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_premium_users_receive_25_percent_discount()

public function test_regular_users_receive_10_percent_discount()
</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Simple tests communicate intent much better than clever ones.</p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-45">8. Slow Tests That Don&#8217;t Need to Be Slow</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>A slow test suite is one of the fastest ways to discourage developers</strong> <strong>from running tests regularly</strong>. If executing your test suite takes ten or fifteen minutes, people naturally start skipping it during development and rely solely on CI. That&#8217;s when bugs begin slipping through.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">The good news is that <strong>PHP itself is rarely the bottleneck</strong>. More often, the slowdown comes from tests doing work they simply don&#8217;t need to do. Calling real third-party APIs, waiting with <code>sleep()</code>, rebuilding large datasets before every test, or repeating expensive setup hundreds of times can quickly turn a fast suite into a painful one.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Modern testing frameworks provide better alternatives for nearly all of these scenarios. Laravel, for example, allows you to fake queues, notifications, mail, events, and storage without sacrificing confidence in your application. Time-dependent code can be tested by freezing or traveling through time instead of waiting in real time, and external services should almost always be replaced with controlled test doubles rather than actual network requests.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>The goal isn&#8217;t to make every individual test as fast as possible. It&#8217;s to eliminate unnecessary work while still testing real application behavior.</strong> A test that finishes in 50 milliseconds instead of 5 seconds might not seem important on its own, but multiply that by thousands of tests running on every pull request, and the difference becomes significant.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Fast feedback encourages developers to run tests frequently. And the more often your tests are executed, the more valuable they become.</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>// ❌ Slow

public function test_export(): void
{
    Storage::disk('s3')->put('report.pdf', '...');
    Mail::send(new ExportFinishedMail());
    Http::get('https://api.example.com');
}</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<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>// ✅ Faster

public function test_export(): void
{
    Storage::fake('s3');
    Mail::fake();
    Http::fake();

    // Test only your application's behavior
}</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">A short PHP example demonstrating this test smell.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>Run on OnlinePHP.io</strong></p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><strong>Try It Yourself:</strong> <a href="https://onlinephp.io/c/b6f80" target="_blank" rel="noreferrer noopener">https://onlinephp.io/c/b6f80</a></p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"></p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-46">9. Giant Tests That Verify Everything</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Almost every long-lived project has at least one of these tests. You open the file expecting to understand a single piece of functionality, only to find a test that&#8217;s hundreds of lines long. It creates users, seeds multiple database records, processes orders, sends emails, dispatches events, updates the cache, writes logs, and finishes with a long list of assertions. At first glance, it feels comprehensive. In reality, it&#8217;s <strong>trying to verify far too many responsibilities at once</strong>.</p>



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

    $response = $this->postJson('/api/orders', &#091;
        'product_id' => $product->id,
    &#093;);

    $response->assertCreated();

    Mail::assertSent(OrderConfirmationMail::class);
    Event::assertDispatched(OrderCreated::class);
    Queue::assertPushed(ProcessInvoice::class);

    $this->assertDatabaseHas('orders', &#091;
        'user_id' => $user->id,
    &#093;);

    $this->assertDatabaseHas('payments', &#091;
        'status' => 'completed',
    &#093;);

    Cache::has("user:{$user->id}:orders");

    Log::shouldHaveReceived('info');
}</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_complete_checkout_process</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">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</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">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</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;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">The biggest problem with these <strong>&#8220;all-in-one&#8221; tests</strong> is that they become extremely fragile over time. When they fail, it&#8217;s often unclear what actually broke. Did the email notification change? Was the database state incorrect? Did an event stop dispatching? Or did a completely unrelated refactor accidentally affect one small part of the workflow? Instead of immediately fixing the issue, developers often spend more time <strong>investigating the failure</strong> than solving the real problem.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">A better approach is to <strong>split large scenarios into smaller, focused tests</strong> that each verify a single aspect of the workflow. One test can ensure an order is created successfully, another can verify that the correct event is dispatched, and a third can confirm that a notification is sent. Together, these tests provide the same level of confidence while remaining significantly easier to understand, maintain, and debug.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Remember that <strong>a test suite is also documentation</strong>. When someone new joins the project, they should be able to read your tests and quickly understand how the application behaves. That&#8217;s almost impossible when one massive test tries to describe an entire business process. Smaller, focused tests produce clearer failure messages, encourage better design, and make future refactoring much less risky.</p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-47">10. Tests Nobody Trusts</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">This is the smell that eventually destroys an entire test suite.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Developers start saying things like:</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><em>&#8220;Oh, just rerun the pipeline.&#8221;</em></p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><em>&#8220;That test always fails randomly.&#8221;</em></p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"><em>&#8220;Ignore that one.&#8221;</em></p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">The moment your team stops trusting the tests, they stop providing value.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Random failures, hidden dependencies, flaky assertions, and inconsistent environments slowly erode confidence until developers treat the test suite as background noise.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">A reliable test suite doesn&#8217;t have to be enormous.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">It has to be predictable.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">If a test fails, developers should immediately believe something is actually wrong.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">That&#8217;s the standard every test suite should aim for.</p>



<hr class="wp-block-separator has-alpha-channel-opacity has-d-4-d-4-d-4-color has-text-color"/>



<h2 class="wp-block-heading has-d-4-d-4-d-4-color has-text-color has-link-color wp-elements-48">Final Thoughts</h2>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Writing tests is only half the job.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Writing tests that remain useful six months later is where the real challenge begins.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Whenever you create a new test, ask yourself a few simple questions:</p>



<ul class="wp-block-list has-d-4-d-4-d-4-color has-text-color">
<li>Does this test verify behavior instead of implementation?</li>



<li>Would another developer understand it without extra context?</li>



<li>Can it run independently?</li>



<li>Will it still be valuable after a refactor?</li>



<li>If this test fails in CI tomorrow, would I trust the failure?</li>
</ul>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">A healthy test suite isn&#8217;t measured by the number of tests it contains or by achieving 100% code coverage.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">It&#8217;s measured by the confidence it gives your team when making changes.</p>



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Because the best tests aren&#8217;t the ones that simply pass—they&#8217;re the ones that catch the bugs that matter.</p>


                <div class="ml-embedded" data-form="tDcAxR"></div>
            



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/08/09/the-most-common-test-smells-in-php-and-how-to-fix-them/">The Most Common Test Smells in PHP (And How to Fix Them)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/08/09/the-most-common-test-smells-in-php-and-how-to-fix-them/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Your Laravel Models Aren’t the Problem. Hidden Workflows Are.</title>
		<link>https://codecraftdiary.com/2026/07/31/laravel-models-business-logic-hidden-workflows/</link>
					<comments>https://codecraftdiary.com/2026/07/31/laravel-models-business-logic-hidden-workflows/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Fri, 31 Jul 2026 11:39:10 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[programming]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3321</guid>

					<description><![CDATA[<p>Most Laravel applications do not become difficult to maintain because somebody made one obviously terrible architectural decision. They get there through dozens of small decisions that looked completely reasonable at the time. A controller grows beyond a comfortable size, so part of its logic moves into an Eloquent model. Later, the application needs to send [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/07/31/laravel-models-business-logic-hidden-workflows/">Your Laravel Models Aren’t the Problem. Hidden Workflows Are.</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Most Laravel applications do not become difficult to maintain because somebody made one obviously terrible architectural decision. They get there through dozens of small decisions that looked completely reasonable at the time.</p>



<p class="wp-block-paragraph">A controller grows beyond a comfortable size, so part of its logic moves into an Eloquent model. Later, the application needs to send a notification after an order ships. Then inventory must be checked through an external API. Accounting requests an ERP integration, and marketing wants loyalty points awarded after dispatch.</p>



<p class="wp-block-paragraph">Each change adds only a few lines. Since the <code>Order</code> model is already available wherever the feature is being implemented, adding one more method feels natural:</p>



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



<p class="wp-block-paragraph">It is concise, expressive, and pleasantly object-oriented. Six months later, however, that innocent method may be updating several tables, calling two APIs, dispatching events, sending notifications, and deciding whether the entire operation is allowed.</p>



<p class="wp-block-paragraph">The model has quietly become the place where the application runs its business workflows.</p>



<p class="wp-block-paragraph">That is the real problem. It is not that the model is “fat.” It is that persistence, domain decisions, and application orchestration have been mixed together until nobody can change one without understanding all three.</p>



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



<h2 class="wp-block-heading">Business Logic in Eloquent Models Is Not Automatically Wrong</h2>



<p class="wp-block-paragraph">The common advice to remove all business logic from Laravel models goes too far.</p>



<p class="wp-block-paragraph">Eloquent follows the Active Record pattern. Martin Fowler describes Active Record as an object that represents a database row, encapsulates database access, and adds domain logic related to that data. In other words, an object containing both state and behavior is not an architectural mistake by itself.</p>



<p class="wp-block-paragraph">A model should be allowed to answer questions about itself:</p>



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

    public function isPaid(): bool
    {
        return $this->paid_at !== null;
    }

    public function isExpired(): bool
    {
        return $this->expires_at?->isPast() ?? false;
    }

    public function hasShippingAddress(): bool
    {
        return filled($this->shipping_address);
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Model</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">protected</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">casts</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">array</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;paid_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;datetime&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;expires_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;datetime&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#093;;</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">isPaid</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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">paid_at</span><span style="color: #D4D4D4"> !== </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<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">isExpired</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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">expires_at</span><span style="color: #D4D4D4">?-&gt;</span><span style="color: #DCDCAA">isPast</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: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">hasShippingAddress</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: #DCDCAA">filled</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">shipping_address</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">These methods are cohesive. They depend only on the order’s own state and make that state easier to understand. Relationships, casts, scopes, accessors, mutators, and similar data-oriented behavior are also natural Eloquent responsibilities; Laravel itself defines relationships as model methods and provides casts specifically for transforming model attributes.</p>



<p class="wp-block-paragraph">The useful distinction is therefore not <strong>business logic versus no business logic</strong>.</p>



<p class="wp-block-paragraph">It is <strong>describing state versus coordinating work</strong>.</p>



<h2 class="wp-block-heading">When a Model Starts Coordinating the Application</h2>



<p class="wp-block-paragraph">Consider a more ambitious implementation of <code>ship()</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>class Order extends Model
{
    public function ship(): void
    {
        if (! $this->isPaid() || ! $this->hasShippingAddress()) {
            throw new DomainException('The order cannot be shipped.');
        }

        $inventoryResponse = Http::get(
            "https://inventory.internal/orders/{$this->id}"
        );

        if (! $inventoryResponse->json('available')) {
            throw new DomainException('The items are not available.');
        }

        DB::transaction(function (): void {
            $this->update(&#091;'status' => 'shipped'&#093;);

            $this->items()->decrement('reserved_quantity');
        });

        Http::post('https://shipping.example.com/dispatch', &#091;
            'order_id' => $this->id,
        &#093;);

        $this->customer->notify(
            new OrderShippedNotification($this)
        );

        event(new OrderShipped($this));
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Model</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">ship</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (! </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isPaid</span><span style="color: #D4D4D4">() || ! </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">hasShippingAddress</span><span style="color: #D4D4D4">()) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">DomainException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;The order cannot be shipped.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$inventoryResponse</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&quot;https://inventory.internal/orders/{</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}&quot;</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (! </span><span style="color: #9CDCFE">$inventoryResponse</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;available&#39;</span><span style="color: #D4D4D4">)) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">DomainException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;The items are not available.&#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: #4EC9B0">DB</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">transaction</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (): </span><span style="color: #569CD6">void</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">update</span><span style="color: #D4D4D4">(&#091;</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;shipped&#39;</span><span style="color: #D4D4D4">&#093;);</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">items</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">decrement</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;reserved_quantity&#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: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;https://shipping.example.com/dispatch&#39;</span><span style="color: #D4D4D4">, &#091;</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: #569CD6">$this</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">        &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">customer</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">notify</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">OrderShippedNotification</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #DCDCAA">event</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderShipped</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">The public API still looks elegant, but the implementation hides a surprising amount of machinery. Calling one method now performs HTTP requests, changes persistent state, updates inventory, sends a notification, and publishes an event.</p>



<p class="wp-block-paragraph">That creates several practical problems.</p>



<p class="wp-block-paragraph">First, its dependencies are invisible. Nothing about <code>$order-&gt;ship()</code> tells the caller that a shipping provider and inventory system must be available. Second, testing one business rule requires faking HTTP requests, notifications, events, and probably a database. Third, every future shipping requirement will be attracted to the same method because it already appears to be the official home of the workflow.</p>



<p class="wp-block-paragraph">The class may still be called <code>Order</code>, but it is acting as an application service.</p>



<h2 class="wp-block-heading">Give the Workflow an Explicit Home</h2>



<p class="wp-block-paragraph">The solution does not require hexagonal architecture, CQRS, repositories for every model, or an interface in front of every class. For many Laravel applications, one focused workflow class is enough.</p>



<p class="wp-block-paragraph">Whether your team calls it an action, command, handler, or service matters less than giving it a clear responsibility.</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\Actions;

use App\Models\Order;
use App\Notifications\OrderShippedNotification;
use App\Services\InventoryService;
use App\Services\ShippingGateway;
use Illuminate\Support\Facades\DB;

final class ShipOrder
{
    public function __construct(
        private InventoryService $inventory,
        private ShippingGateway $shipping,
    ) {
    }

    public function handle(Order $order): void
    {
        $this->ensureOrderCanBeShipped($order);

        $shipment = $this->shipping->dispatch($order);

        DB::transaction(function () use ($order, $shipment): void {
            $order->update(&#091;
                'status' => 'shipped',
                'shipment_reference' => $shipment->reference,
                'shipped_at' => now(),
            &#093;);
        });

        $order->customer->notify(
            new OrderShippedNotification($order)
        );
    }

    private function ensureOrderCanBeShipped(Order $order): void
    {
        if (! $order->isPaid()) {
            throw new CannotShipOrder('The order has not been paid.');
        }

        if (! $order->hasShippingAddress()) {
            throw new CannotShipOrder('The shipping address is missing.');
        }

        if (! $this->inventory->isAvailableFor($order)) {
            throw new CannotShipOrder('One or more items are unavailable.');
        }
    }
}</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\Actions</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">Order</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Notifications\</span><span style="color: #4EC9B0">OrderShippedNotification</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Services\</span><span style="color: #4EC9B0">InventoryService</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Services\</span><span style="color: #4EC9B0">ShippingGateway</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Support\Facades\</span><span style="color: #4EC9B0">DB</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">final</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShipOrder</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">InventoryService</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShippingGateway</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$shipping</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">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">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">ensureOrderCanBeShipped</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: #9CDCFE">$shipment</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">shipping</span><span style="color: #D4D4D4">-&gt;</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: #4EC9B0">DB</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">transaction</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> () </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$shipment</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">update</span><span style="color: #D4D4D4">(&#091;</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;shipped&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;shipment_reference&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$shipment</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">reference</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;shipped_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">            &#093;);</span></span>
<span class="line"><span style="color: #D4D4D4">        });</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">customer</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">notify</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">OrderShippedNotification</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">ensureOrderCanBeShipped</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (! </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isPaid</span><span style="color: #D4D4D4">()) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CannotShipOrder</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;The order has not been paid.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (! </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">hasShippingAddress</span><span style="color: #D4D4D4">()) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CannotShipOrder</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;The shipping address is missing.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (! </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">inventory</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isAvailableFor</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">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CannotShipOrder</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;One or more items are unavailable.&#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></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">The controller remains small without forcing the workflow into the model:</p>



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

        return response()->json(&#091;
            'message' => 'Order shipped successfully.',
        &#093;);
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">final</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShipOrderController</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 style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #4EC9B0">ShipOrder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$shipOrder</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    ): </span><span style="color: #4EC9B0">JsonResponse</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$shipOrder</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(&#091;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;message&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Order shipped successfully.&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#093;);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">This is not better merely because the code lives in another file. It is better because the dependencies and responsibility are now explicit. <code>InventoryService</code> answers an inventory question. <code>ShippingGateway</code> communicates with the shipping provider. <code>ShipOrder</code> coordinates the use case. The <code>Order</code> model describes and persists order state.</p>



<p class="wp-block-paragraph">Laravel’s transaction helper also gives the persistence portion a clear boundary and automatically rolls it back when an exception is thrown.</p>



<h2 class="wp-block-heading">Avoid Building a Fat Action Instead</h2>



<p class="wp-block-paragraph">Extracting a workflow does not give it permission to become another dumping ground.</p>



<p class="wp-block-paragraph">If <code>ShipOrder</code> eventually contains payment processing, PDF generation, ERP payload construction, low-level HTTP calls, and hundreds of lines of unrelated calculations, the architecture has not improved much. The oversized model has simply been replaced by an oversized action.</p>



<p class="wp-block-paragraph">A workflow class should coordinate collaborators rather than implement every collaborator itself. External API details belong behind focused clients or gateways. Complicated calculations may deserve dedicated domain objects. Slow, retryable work can be handed to queued jobs; Laravel provides queues specifically for deferring work across several supported backends.</p>



<p class="wp-block-paragraph">Real integrations also introduce failure scenarios that a short example cannot solve completely. A shipping provider might accept a request just before the database update fails. Production systems may therefore need idempotency keys, retry policies, an outbox, or an intermediate status such as <code>shipping_pending</code>. The important point is that these concerns become easier to see once the workflow has an explicit home.</p>



<h2 class="wp-block-heading">A Rule That Is Actually Useful</h2>



<p class="wp-block-paragraph">When deciding where a new method belongs, ask one question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Does this method describe the object, or does it coordinate the application?</p>
</blockquote>



<p class="wp-block-paragraph">These methods usually fit naturally on a model:</p>



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



<p class="wp-block-paragraph">These operations deserve closer inspection:</p>



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



<div class="wp-block-group is-nowrap is-layout-flex wp-container-core-group-is-layout-995f960e wp-block-group-is-layout-flex">
<p class="wp-block-paragraph">A method name alone does not decide the answer. An <code>Order::ship()</code> method that only performs a valid local state transition may be perfectly reasonable. An <code>Order::ship()</code> method that calls APIs, sends emails, updates unrelated records, and queues jobs is a hidden workflow.</p>



<p class="wp-block-paragraph"><a href="https://codecraftdiary.com/2026/07/31/laravel-models-business-logic-hidden-workflows/" target="_blank" rel="noreferrer noopener"></a></p>
</div>



<p class="wp-block-paragraph">Good Laravel architecture is not about producing the largest possible number of small classes. It is about making the next change obvious.</p>



<p class="wp-block-paragraph">Your Eloquent models can contain meaningful behavior. They should understand their relationships, attributes, and state. What they should not quietly become is the control room for the rest of the application.</p>



<p class="wp-block-paragraph">When workflows have explicit homes and dependencies have clear boundaries, controllers stay readable, models remain coherent, and future developers do not need to reverse-engineer half the codebase to discover what happens when an order ships.</p>



<p class="wp-block-paragraph">The models were never the real enemy.</p>



<p class="wp-block-paragraph">The hidden workflows were.</p>


                <div class="ml-embedded" data-form="tDcAxR"></div>
            
<p>The post <a href="https://codecraftdiary.com/2026/07/31/laravel-models-business-logic-hidden-workflows/">Your Laravel Models Aren’t the Problem. Hidden Workflows Are.</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/07/31/laravel-models-business-logic-hidden-workflows/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Merge Conflicts Are a Process Problem, Not a Git Problem</title>
		<link>https://codecraftdiary.com/2026/07/26/merge-conflicts-process-problem/</link>
					<comments>https://codecraftdiary.com/2026/07/26/merge-conflicts-process-problem/#comments</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 26 Jul 2026 14:24:34 +0000</pubDate>
				<category><![CDATA[Development Workflow & Best Practices]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[devops]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[software-design]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3318</guid>

					<description><![CDATA[<p>Most developers treat merge conflicts as a Git problem. Git tells you that two people changed the same code, highlights the conflicting lines, and leaves you to sort it out. After a few minutes of editing, you commit the resolution and move on. It feels like Git is the problem. But after working on enough [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/07/26/merge-conflicts-process-problem/">Merge Conflicts Are a Process Problem, Not a Git Problem</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Most developers treat merge conflicts as a Git problem.</p>



<p class="wp-block-paragraph">Git tells you that two people changed the same code, highlights the conflicting lines, and leaves you to sort it out. After a few minutes of editing, you commit the resolution and move on.</p>



<p class="wp-block-paragraph">It feels like Git is the problem.</p>



<p class="wp-block-paragraph">But after working on enough backend projects, I&#8217;ve come to a different conclusion.</p>



<p class="wp-block-paragraph">Merge conflicts are rarely caused by Git itself.</p>



<p class="wp-block-paragraph">Git is simply exposing a problem that already exists in your development process.</p>



<p class="wp-block-paragraph">If your team spends hours every week resolving merge conflicts, the real issue probably isn&#8217;t your version control system. It&#8217;s how your team works.</p>



<p class="wp-block-paragraph">Last article in this category: <a href="https://codecraftdiary.com/2026/06/28/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/06/28/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline/</a></p>



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



<h1 class="wp-block-heading">Git Isn&#8217;t Creating the Conflict</h1>



<p class="wp-block-paragraph">One thing that&#8217;s easy to forget is that Git doesn&#8217;t invent conflicts.</p>



<p class="wp-block-paragraph">A merge conflict only appears when Git genuinely cannot determine which change should win.</p>



<p class="wp-block-paragraph">Imagine two developers working on the same controller.</p>



<p class="wp-block-paragraph">Developer A adds request validation.</p>



<p class="wp-block-paragraph">Developer B changes the response format.</p>



<p class="wp-block-paragraph">Both modify the same method.</p>



<p class="wp-block-paragraph">Git stops and asks someone to make the decision because it simply doesn&#8217;t have enough context.</p>



<p class="wp-block-paragraph">That&#8217;s exactly what it&#8217;s supposed to do.</p>



<p class="wp-block-paragraph">The conflict isn&#8217;t a Git failure.</p>



<p class="wp-block-paragraph">The failure happened much earlier—when two developers unknowingly spent days modifying the same piece of code.</p>



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



<h1 class="wp-block-heading">The Real Question Is: Why Did This Happen?</h1>



<p class="wp-block-paragraph">Whenever someone tells me:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;Git merge conflicts are driving me crazy.&#8221;</p>
</blockquote>



<p class="wp-block-paragraph">I usually ask one question.</p>



<p class="wp-block-paragraph"><strong>How long was your branch open?</strong></p>



<p class="wp-block-paragraph">More often than not, the answer is:</p>



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



<li>one week</li>



<li>sometimes even three weeks</li>
</ul>



<p class="wp-block-paragraph">That&#8217;s usually where the real problem begins.</p>



<p class="wp-block-paragraph">Every day your branch stays isolated, the main branch keeps moving.</p>



<p class="wp-block-paragraph">Someone renames classes.</p>



<p class="wp-block-paragraph">Someone refactors a service.</p>



<p class="wp-block-paragraph">Someone fixes a production bug.</p>



<p class="wp-block-paragraph">Someone updates dependencies.</p>



<p class="wp-block-paragraph">By the time you&#8217;re ready to merge, your code is no longer based on today&#8217;s project—it&#8217;s based on the project from last week.</p>



<p class="wp-block-paragraph">Git is simply the first tool to point that out.</p>



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



<h1 class="wp-block-heading">The Hidden Cost Isn&#8217;t the Conflict</h1>



<p class="wp-block-paragraph">People often think merge conflicts are expensive because they take time to resolve.</p>



<p class="wp-block-paragraph">In reality, editing the conflicting lines usually takes only a few minutes.</p>



<p class="wp-block-paragraph">Understanding <strong>why</strong> the conflict exists is what costs time.</p>



<p class="wp-block-paragraph">Imagine this situation.</p>



<p class="wp-block-paragraph">You started a feature on Monday.</p>



<p class="wp-block-paragraph">By Friday:</p>



<ul class="wp-block-list">
<li>six pull requests have been merged</li>



<li>your service has been partially refactored</li>



<li>another developer extracted shared logic into a new class</li>



<li>someone fixed a production bug in the same module</li>
</ul>



<p class="wp-block-paragraph">Now Git reports a conflict.</p>



<p class="wp-block-paragraph">You&#8217;re no longer solving one conflict.</p>



<p class="wp-block-paragraph">You&#8217;re rebuilding your mental model of everything that happened while you were working.</p>



<p class="wp-block-paragraph">That&#8217;s the expensive part.</p>



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



<h1 class="wp-block-heading">Large Pull Requests Multiply the Problem</h1>



<p class="wp-block-paragraph">One pattern appears almost everywhere.</p>



<p class="wp-block-paragraph">Large pull requests almost always create larger merge conflicts.</p>



<p class="wp-block-paragraph">It isn&#8217;t surprising.</p>



<p class="wp-block-paragraph">A pull request with 2,000 changed lines touches dozens of files.</p>



<p class="wp-block-paragraph">A pull request with 150 lines might touch only two or three.</p>



<p class="wp-block-paragraph">Which one is more likely to overlap with someone else&#8217;s work?</p>



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



<p class="wp-block-paragraph">This is one of the biggest reasons I prefer small pull requests.</p>



<p class="wp-block-paragraph">Smaller changes don&#8217;t just make reviews easier.</p>



<p class="wp-block-paragraph">They dramatically reduce the chance that multiple developers modify the same code at the same time.</p>



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



<h1 class="wp-block-heading">Long-Lived Branches Drift Away From Reality</h1>



<p class="wp-block-paragraph">Feature branches aren&#8217;t the problem.</p>



<p class="wp-block-paragraph">Long-lived feature branches are.</p>



<p class="wp-block-paragraph">I&#8217;ve seen teams spend two weeks building a feature before opening their first pull request.</p>



<p class="wp-block-paragraph">By then, the main branch has evolved so much that merging becomes an entire task on its own.</p>



<p class="wp-block-paragraph">Developers spend hours:</p>



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



<li>updating failing tests</li>



<li>adapting to refactored services</li>



<li>resolving dependency changes</li>
</ul>



<p class="wp-block-paragraph">Ironically, teams often conclude that merging is painful.</p>



<p class="wp-block-paragraph">The truth is slightly different.</p>



<p class="wp-block-paragraph"><strong>Merging became painful because they waited too long.</strong></p>



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



<h1 class="wp-block-heading">Communication Prevents More Conflicts Than Git Ever Will</h1>



<p class="wp-block-paragraph">Not every conflict can be avoided.</p>



<p class="wp-block-paragraph">Sometimes two developers genuinely need to work in the same part of the application.</p>



<p class="wp-block-paragraph">The difference is whether they know about each other.</p>



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



<p class="wp-block-paragraph">One developer is refactoring the authentication service.</p>



<p class="wp-block-paragraph">Another developer is implementing passwordless login.</p>



<p class="wp-block-paragraph">Without communication, both spend several days modifying the same code.</p>



<p class="wp-block-paragraph">The conflict is inevitable.</p>



<p class="wp-block-paragraph">Now imagine a five-minute conversation before they start.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;I&#8217;m working on AuthService this week.&#8221;</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;Me too.&#8221;</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">&#8220;Let&#8217;s split the work and merge more often.&#8221;</p>
</blockquote>



<p class="wp-block-paragraph">Five minutes of communication can save hours of unnecessary conflict resolution.</p>



<p class="wp-block-paragraph">Git could never solve that problem.</p>



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



<h1 class="wp-block-heading">Trunk-Based Development Changes Everything</h1>



<p class="wp-block-paragraph">This is one of the biggest reasons I&#8217;m such a fan of Trunk-Based Development.</p>



<p class="wp-block-paragraph">The goal isn&#8217;t to eliminate merge conflicts completely.</p>



<p class="wp-block-paragraph">That&#8217;s impossible.</p>



<p class="wp-block-paragraph">The goal is to make them so small that nobody really cares.</p>



<p class="wp-block-paragraph">Instead of merging once every Friday&#8230;</p>



<p class="wp-block-paragraph">&#8230;developers merge several times every day.</p>



<p class="wp-block-paragraph">Instead of resolving fifty conflicts at once&#8230;</p>



<p class="wp-block-paragraph">&#8230;they resolve one tiny overlap before lunch.</p>



<p class="wp-block-paragraph">Small branches create small pull requests.</p>



<p class="wp-block-paragraph">Small pull requests create small conflicts.</p>



<p class="wp-block-paragraph">Small conflicts rarely interrupt development.</p>



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



<h1 class="wp-block-heading">The Most Dangerous Conflicts Never Appear in Git</h1>



<p class="wp-block-paragraph">Interestingly, the worst conflicts aren&#8217;t merge conflicts at all.</p>



<p class="wp-block-paragraph">Imagine this scenario.</p>



<p class="wp-block-paragraph">Developer A changes how discounts are calculated.</p>



<p class="wp-block-paragraph">Developer B changes invoice generation.</p>



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



<p class="wp-block-paragraph">No merge conflict.</p>



<p class="wp-block-paragraph">Everything merges perfectly.</p>



<p class="wp-block-paragraph">Production breaks because invoice generation still expects the previous discount logic.</p>



<p class="wp-block-paragraph">Git couldn&#8217;t detect that.</p>



<p class="wp-block-paragraph">The problem wasn&#8217;t textual.</p>



<p class="wp-block-paragraph">It was logical.</p>



<p class="wp-block-paragraph">These hidden conflicts become much easier to discover when teams integrate continuously instead of waiting days before merging.</p>



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



<h1 class="wp-block-heading">How to Reduce Merge Conflicts</h1>



<p class="wp-block-paragraph">No workflow eliminates merge conflicts completely.</p>



<p class="wp-block-paragraph">But a few simple habits make them significantly less painful.</p>



<ul class="wp-block-list">
<li>Merge into the main branch frequently.</li>



<li>Keep pull requests small.</li>



<li>Avoid combining unrelated changes.</li>



<li>Communicate when multiple developers work in the same area.</li>



<li>Use feature flags instead of long-lived branches.</li>



<li>Review pull requests quickly so they don&#8217;t sit idle for days.</li>
</ul>



<p class="wp-block-paragraph">None of these practices are revolutionary.</p>



<p class="wp-block-paragraph">Together, however, they change merge conflicts from major interruptions into minor inconveniences.</p>



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



<h1 class="wp-block-heading">Stop Blaming Git</h1>



<p class="wp-block-paragraph">Git has become one of the easiest tools to blame.</p>



<p class="wp-block-paragraph">It reports the conflict.</p>



<p class="wp-block-paragraph">It highlights the differences.</p>



<p class="wp-block-paragraph">It forces developers to make a decision.</p>



<p class="wp-block-paragraph">But Git didn&#8217;t create the problem.</p>



<p class="wp-block-paragraph">It simply revealed it.</p>



<p class="wp-block-paragraph">The next time your team spends an afternoon resolving merge conflicts, don&#8217;t ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>&#8220;How can we get better at Git?&#8221;</strong></p>
</blockquote>



<p class="wp-block-paragraph">Ask something much more valuable:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>&#8220;What in our development process allowed this conflict to grow this large?&#8221;</strong></p>
</blockquote>



<p class="wp-block-paragraph">Most of the time, the answer isn&#8217;t hidden in a Git command.</p>



<p class="wp-block-paragraph">It&#8217;s hidden in the workflow.</p>



<p class="wp-block-paragraph">And once the workflow improves, merge conflicts become exactly what they should be: small, occasional interruptions instead of recurring team-wide headaches.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/07/26/merge-conflicts-process-problem/">Merge Conflicts Are a Process Problem, Not a Git Problem</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/07/26/merge-conflicts-process-problem/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<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-55">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-56">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-57">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-58">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-59">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-60">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-70">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-71">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-72">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-73">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-74">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-75">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-76">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-77">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-78">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-85">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-86">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-87">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-88">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-89">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-90">Conclusion: Speed Breeds Quality</h2>



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">We start with a feature test. We are defining the <em>behavior</em>, not the implementation.</p>



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

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



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



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



<p class="wp-block-paragraph">Now, we write just enough code to make this pass. We might put all the logic in the controller (yes, even if it&#8217;s dirty, because that&#8217;s the &#8220;Green&#8221; phase).</p>



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



<p class="wp-block-paragraph">Test passes. <strong>Green.</strong></p>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



<p class="wp-block-paragraph">Consider a typical registration process in a Laravel application:</p>



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

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

    return response()->json(&#91;'message' => 'Success'&#93;, 201);
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">register</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">RegisterRequest</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">validated</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// Tightly coupled dependencies</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">to</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">send</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">WelcomeEmail</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">info</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;User registered: &#39;</span><span style="color: #D4D4D4"> </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Analytics</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">track</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;user_signup&#39;</span><span style="color: #D4D4D4">, &#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Newsletter</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">subscribe</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">email</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;message&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Success&#39;</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #B5CEA8">201</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">This code is intuitive, yet architecturally toxic. The controller is burdened with infrastructure knowledge. If your <code>Newsletter</code> API slows down, your user registration experiences lag. If the <code>Mail</code> service throws an exception, the entire request fails, potentially causing data inconsistency or frustration. You have created a <strong>brittle system</strong> where the success of a core operation depends on four secondary services.</p>



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



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



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



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



<p class="wp-block-paragraph">Think of an event as a &#8220;data transfer object&#8221; that carries necessary context.</p>



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



<h3 class="wp-block-heading">2. The Lean Controller</h3>



<p class="wp-block-paragraph">Now, look at how the controller looks after refactoring:</p>



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

    return response()->json(&#91;'message' => 'Success'&#93;, 201);
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">register</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">RegisterRequest</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">validated</span><span style="color: #D4D4D4">());</span></span>
<span class="line"><span style="color: #D4D4D4">    </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// Announce the action</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">UserRegistered</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;message&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Success&#39;</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #B5CEA8">201</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">By decoupling, the controller is now focused solely on persistence and orchestration, not on the side effects.</p>



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



<p class="wp-block-paragraph">The true power of events in Laravel emerges when you utilize background processing. By implementing the <code>ShouldQueue</code> interface, you move the heavy lifting away from the HTTP request cycle.</p>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">When you move to <code>ShouldQueue</code>, you must plan for failure. In Laravel, you can define how your listeners handle retries:</p>



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



<p class="wp-block-paragraph">This ensures that temporary outages (like a flickering API connection) don&#8217;t result in lost data.</p>



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



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



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/">Beyond Fat Controllers: Mastering Event-Driven Decoupling in Laravel</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
