<?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>Refactoring &amp; Patterns Archives - CodeCraft Diary</title>
	<atom:link href="https://codecraftdiary.com/category/refactoring-patterns/feed/" rel="self" type="application/rss+xml" />
	<link>https://codecraftdiary.com/category/refactoring-patterns/</link>
	<description></description>
	<lastBuildDate>Sun, 05 Jul 2026 08:41:37 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.2</generator>

<image>
	<url>https://codecraftdiary.com/wp-content/uploads/2025/10/cropped-IMG_3463-32x32.png</url>
	<title>Refactoring &amp; Patterns Archives - CodeCraft Diary</title>
	<link>https://codecraftdiary.com/category/refactoring-patterns/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>The Specification Pattern in Laravel: Refactoring Complex Business Rules</title>
		<link>https://codecraftdiary.com/2026/07/06/specification-pattern-in-laravel/</link>
					<comments>https://codecraftdiary.com/2026/07/06/specification-pattern-in-laravel/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 13:45:00 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3311</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">Typically, you see developers solving this in two ways. First, they write a massive query in a controller or service:</p>



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



<p class="wp-block-paragraph">This works fine for fetching data from the database. But what happens when you already have a <code class="">User</code> instance in memory—say, inside a Job or an Event Listener—and you need to check if <em>this specific user</em> qualifies?</p>



<p class="wp-block-paragraph">You end up duplicating the logic in pure PHP:</p>



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



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-a89d0507d8998c98779e0aba50a651ac">Why this breaks the KISS principle:</h3>



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



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



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



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



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



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



<p class="wp-block-paragraph">Let&#8217;s define a simple interface for our specifications:</p>



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

use Illuminate\Database\Eloquent\Builder;

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

    /**
     * Apply the specification directly to an Eloquent query builder.
     */
    public function apply(Builder $query): void;
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Specifications</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Database\Eloquent\</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">interface</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Specification</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">/**</span></span>
<span class="line"><span style="color: #6A9955">     * Check if a given object satisfies the specification in-memory.</span></span>
<span class="line"><span style="color: #6A9955">     */</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">mixed</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">bool</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">/**</span></span>
<span class="line"><span style="color: #6A9955">     * Apply the specification directly to an Eloquent query builder.</span></span>
<span class="line"><span style="color: #6A9955">     */</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">apply</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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



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

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

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

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

    public function apply(Builder $query): void
    {
        $query->where('is_active', true)
            ->whereNotNull('email_verified_at')
            ->whereHas('orders', function ($q) {
                $q->where('status', 'completed');
            }, '>=', 5)
            ->whereDoesntHave('flags', function ($q) {
                $q->where('type', 'fraud');
            });
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Specifications</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Models\</span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Database\Eloquent\</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">IsVipCustomer</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Specification</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">mixed</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">bool</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (!</span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4"> instanceof </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">false</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">is_active</span></span>
<span class="line"><span style="color: #D4D4D4">            &amp;&amp; </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">email_verified_at</span><span style="color: #D4D4D4"> !== </span><span style="color: #569CD6">null</span></span>
<span class="line"><span style="color: #D4D4D4">            &amp;&amp; </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">orders</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;completed&#39;</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">count</span><span style="color: #D4D4D4">() &gt;= </span><span style="color: #B5CEA8">5</span></span>
<span class="line"><span style="color: #D4D4D4">            &amp;&amp; !</span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">flags</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">contains</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;type&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;fraud&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">apply</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;is_active&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">whereNotNull</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;email_verified_at&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">whereHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;completed&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">            }, </span><span style="color: #CE9178">&#39;&gt;=&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">whereDoesntHave</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;flags&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;type&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;fraud&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">            });</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Notice how clean this is. The technical details of <em>how</em> we determine a VIP customer are encapsulated in exactly one file.</p>



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



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



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



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

use Illuminate\Database\Eloquent\Builder;

class AndSpecification implements Specification
{
    private array $specifications;

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

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

    public function apply(Builder $query): void
    {
        foreach ($this->specifications as $spec) {
            $spec->apply($query);
        }
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Specifications</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Database\Eloquent\</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">AndSpecification</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Specification</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$specifications</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Specification</span><span style="color: #D4D4D4"> ...</span><span style="color: #9CDCFE">$specifications</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">specifications</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$specifications</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">mixed</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">bool</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">specifications</span><span style="color: #D4D4D4"> as </span><span style="color: #9CDCFE">$spec</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (!</span><span style="color: #9CDCFE">$spec</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$candidate</span><span style="color: #D4D4D4">)) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">false</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">            }</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">apply</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">specifications</span><span style="color: #D4D4D4"> as </span><span style="color: #9CDCFE">$spec</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$spec</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">apply</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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



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



<p class="wp-block-paragraph">Inside your Admin Dashboard controller, you need to list all VIP customers from the EU to send them a special newsletter.</p>



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

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

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

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

        return view('admin.vip-newsletter', compact('users'));
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Http\Controllers</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Models\</span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Specifications\</span><span style="color: #4EC9B0">AndSpecification</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Specifications\</span><span style="color: #4EC9B0">IsVipCustomer</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Specifications\</span><span style="color: #4EC9B0">IsLocatedInEU</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">VipNewsletterController</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Controller</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__invoke</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Define the composite rule</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$vipInEuSpec</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">AndSpecification</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">IsVipCustomer</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">IsLocatedInEU</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Apply it directly to the query</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">query</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$vipInEuSpec</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">apply</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$users</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">paginate</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">30</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">view</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;admin.vip-newsletter&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #DCDCAA">compact</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;users&#39;</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">Scenario B: In-Memory Domain Validation</h3>



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



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

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

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

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

        // Process reward...
        return response()->json(&#91;'success' => 'Reward claimed!'&#93;);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Http\Controllers</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Specifications\</span><span style="color: #4EC9B0">IsVipCustomer</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Http\</span><span style="color: #4EC9B0">Request</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">RewardController</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Controller</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">claim</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Request</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">IsVipCustomer</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$vipSpec</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">user</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Check the exact same business rule in-memory</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (!</span><span style="color: #9CDCFE">$vipSpec</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isSatisfiedBy</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">)) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;error&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;You do not qualify for this reward.&#39;</span></span>
<span class="line"><span style="color: #D4D4D4">            &#93;, </span><span style="color: #B5CEA8">403</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Process reward...</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;success&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Reward claimed!&#39;</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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



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



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



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



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



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



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

    $spec = new IsVipCustomer();

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



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



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



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



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



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



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



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



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

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



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



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



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



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



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

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

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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/">Beyond Fat Controllers: Mastering Event-Driven Decoupling in Laravel</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>State Pattern vs. Enums in Modern PHP</title>
		<link>https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/</link>
					<comments>https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 25 May 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<category><![CDATA[software-design]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3275</guid>

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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">For simple workflows, Enums are clean and maintainable.</p>



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



<p class="wp-block-paragraph">This is ideal when states are primarily used for:</p>



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



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



<li>Basic validation</li>



<li>API serialization</li>



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



<p class="wp-block-paragraph">Problems start appearing when business rules become state-dependent. A common first step is adding helper methods directly into the Enum:</p>



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

    public function canBeCancelled(): bool
    {
        return match($this) {
            self::Pending, self::Paid => true,
            self::Shipped, self::Cancelled => false,
        };
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">enum</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStatus</span><span style="color: #D4D4D4">: </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Pending = </span><span style="color: #CE9178">&#39;pending&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Paid = </span><span style="color: #CE9178">&#39;paid&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Shipped = </span><span style="color: #CE9178">&#39;shipped&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Cancelled = </span><span style="color: #CE9178">&#39;cancelled&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">canBeCancelled</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">bool</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #C586C0">match</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Pending, </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Paid </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Shipped, </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Cancelled </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">false</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        };</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">This is still perfectly reasonable and respects the KISS principle.</p>



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



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



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



<p class="wp-block-paragraph">Consider how a bloating Enum typically looks in production:</p>



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

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

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

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

    private function executeCancellationWithRefund(Order $order, PaymentGateway $gateway, InventoryManager $inventory): void
    {
        $gateway->refund($order->payment_id);
        $inventory->restock($order->items);
        $order->updateStatus(self::Cancelled);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">enum</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStatus</span><span style="color: #D4D4D4">: </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Pending = </span><span style="color: #CE9178">&#39;pending&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Paid = </span><span style="color: #CE9178">&#39;paid&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Shipped = </span><span style="color: #CE9178">&#39;shipped&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">case</span><span style="color: #D4D4D4"> Cancelled = </span><span style="color: #CE9178">&#39;cancelled&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">cancel</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #4EC9B0">PaymentGateway</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #4EC9B0">InventoryManager</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</span></span>
<span class="line"><span style="color: #D4D4D4">    ): </span><span style="color: #569CD6">void</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">match</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Pending </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">updateStatus</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Cancelled),</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Paid </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">executeCancellationWithRefund</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$inventory</span><span style="color: #D4D4D4">),</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Shipped </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Cannot cancel a shipped order.&#39;</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Cancelled </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Order is already cancelled.&#39;</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">        };</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">executeCancellationWithRefund</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">PaymentGateway</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">InventoryManager</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">refund</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">payment_id</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$inventory</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">restock</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">items</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">updateStatus</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::Cancelled);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">The issue here is not the number of lines. <strong>The issue is coupling.</strong></p>



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



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



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



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



<h3 class="wp-block-heading">1. Define the Workflow Contract</h3>



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



<p class="wp-block-paragraph">The interface should stay minimal. Only include operations whose behavior actually changes depending on the state.</p>



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



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



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



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

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

    public function toValue(): string
    {
        return 'pending';
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PendingState</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderState</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">cancel</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">transitionTo</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CancelledState</span><span style="color: #D4D4D4">());</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">ship</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Cannot ship an unpaid order.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">toValue</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #CE9178">&#39;pending&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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

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

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

    public function toValue(): string
    {
        return 'paid';
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PaidState</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderState</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PaymentGateway</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">InventoryManager</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">cancel</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">gateway</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">refund</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">payment_id</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">inventory</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">restock</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">items</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">transitionTo</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CancelledState</span><span style="color: #D4D4D4">());</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">ship</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">transitionTo</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShippedState</span><span style="color: #D4D4D4">());</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">toValue</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #CE9178">&#39;paid&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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

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

    public function toValue(): string
    {
        return 'shipped';
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShippedState</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">implements</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderState</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">cancel</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;The order has already been shipped.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">ship</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">LogicException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Order is already shipped.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">toValue</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #CE9178">&#39;shipped&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Most importantly, adding a new state no longer requires modifying a massive conditional block. This aligns naturally with the <strong>Open/Closed Principle</strong>.</p>



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



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



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



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



<p class="wp-block-paragraph">The Enum remains the canonical storage format:</p>



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



<h3 class="wp-block-heading">Resolving State Behavior Cleanly</h3>



<p class="wp-block-paragraph">Instead of resolving dependencies directly inside the model, a dedicated factory keeps infrastructure concerns isolated from your domain.</p>



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

    public function make(OrderStatusEnum $status): OrderState
    {
        return match ($status) {
            OrderStatusEnum::Pending => new PendingState(),
            OrderStatusEnum::Paid => new PaidState($this->gateway, $this->inventory),
            OrderStatusEnum::Shipped => new ShippedState(),
            OrderStatusEnum::Cancelled => new CancelledState(),
        };
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStateFactory</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PaymentGateway</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$gateway</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">InventoryManager</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">make</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$status</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">OrderState</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #C586C0">match</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$status</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">::Pending </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PendingState</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">::Paid </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">PaidState</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">gateway</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">inventory</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">::Shipped </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShippedState</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">::Cancelled </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CancelledState</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">        };</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">Keeping the Model Lightweight</h3>



<p class="wp-block-paragraph">Now, the <code>Order</code> model stays clean and decoupled from infrastructure services. It simply orchestrates the workflow via the factory:</p>



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

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

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

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

    public function ship(): void
    {
        $this->stateFactory
            ->make($this->status)
            ->ship($this);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$status</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$payment_id</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$items</span><span style="color: #D4D4D4"> = [];</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderStateFactory</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$stateFactory</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">transitionTo</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderState</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$state</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">status</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">OrderStatusEnum</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">from</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$state</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">toValue</span><span style="color: #D4D4D4">());</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">cancel</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stateFactory</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">make</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">status</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">cancel</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">ship</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stateFactory</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">make</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">status</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">ship</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">This keeps your persistence simple, business logic isolated, dependencies explicit, and workflows extensible—all without introducing heavy framework abstractions.</p>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">The State Pattern is valuable not because it is “more advanced,” but because it isolates change. And in long-lived systems, isolated change is usually what keeps complexity manageable.</p>
<p>The post <a href="https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/">State Pattern vs. Enums in Modern PHP</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
