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

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

<image>
	<url>https://codecraftdiary.com/wp-content/uploads/2025/10/cropped-IMG_3463-32x32.png</url>
	<title>codecraftdiary, Author at CodeCraft Diary</title>
	<link>https://codecraftdiary.com/author/jwchildroot/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Testing Authorization in Laravel: Policies, Gates, and Middleware</title>
		<link>https://codecraftdiary.com/2026/10/11/testing-authorization-laravel-policies-gates-middleware/</link>
					<comments>https://codecraftdiary.com/2026/10/11/testing-authorization-laravel-policies-gates-middleware/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 11 Oct 2026 18:10:09 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3398</guid>

					<description><![CDATA[<p>Most Laravel test suites I&#8217;ve seen have the same blind spot. They test that a user can edit their own post. They test that the admin dashboard loads for an admin. CI is green, everyone is happy. What they don&#8217;t test is whether a random logged-in user can edit someone else&#8217;s post. And that&#8217;s exactly [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/10/11/testing-authorization-laravel-policies-gates-middleware/">Testing Authorization in Laravel: Policies, Gates, and Middleware</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Most Laravel test suites I&#8217;ve seen have the same blind spot.</p>



<p class="wp-block-paragraph">They test that a user <em>can</em> edit their own post. They test that the admin dashboard loads for an admin. CI is green, everyone is happy.</p>



<p class="wp-block-paragraph">What they don&#8217;t test is whether a random logged-in user can edit <em>someone else&#8217;s</em> post.</p>



<p class="wp-block-paragraph">And that&#8217;s exactly the kind of bug that ends up in a security report.</p>



<p class="wp-block-paragraph">Authorization feels &#8220;done&#8221; the moment you write the policy. But a policy nobody calls, a middleware someone removed from a route group during a refactor, or a gate with a flipped condition won&#8217;t break a single happy-path test. It just quietly leaves a door open.</p>



<p class="wp-block-paragraph">In this article I&#8217;ll show you how I approach <strong>testing authorization in Laravel</strong>: policies, gates, and middleware. Nothing fancy, just patterns that keep these tests short, readable, and actually useful.</p>



<p class="wp-block-paragraph">Last article in this category: <a href="https://codecraftdiary.com/2026/09/06/testing-time-in-laravel/">https://codecraftdiary.com/2026/09/06/testing-time-in-laravel/</a></p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-10">The Example: A Simple Post Policy</h2>



<p class="wp-block-paragraph">Let&#8217;s keep it realistic but small. Users write posts. Only the author can update a post. The author or an admin can delete it.</p>



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

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || $user->is_admin;
    }
}</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">PostPolicy</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">update</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: #4EC9B0">Post</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$post</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">id</span><span style="color: #D4D4D4"> === </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">user_id</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">delete</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: #4EC9B0">Post</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$post</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">id</span><span style="color: #D4D4D4"> === </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">user_id</span><span style="color: #D4D4D4"> || </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">is_admin</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">And the controller uses 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>public function update(Request $request, Post $post)
{
    Gate::authorize('update', $post);

    $post->update($request->validate([
        'title' => &#91;'required', 'string', 'max:255'&#93;,
    ]));

    return redirect()->route('posts.show', $post);
}</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">update</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">Post</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$post</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">Gate</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">authorize</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;update&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">update</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">validate</span><span style="color: #D4D4D4">([</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;title&#39;</span><span style="color: #D4D4D4"> =&gt; &#91;</span><span style="color: #CE9178">&#39;required&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;string&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;max:255&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">    ]));</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">redirect</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">route</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;posts.show&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">There are two separate questions here, and you need tests for both:</p>



<ol class="wp-block-list">
<li>Is the policy logic correct?</li>



<li>Is the policy actually used where it should be?</li>
</ol>



<p class="wp-block-paragraph">Most people only test the first one. Some test neither.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-11">Step 1: Unit Testing the Policy</h2>



<p class="wp-block-paragraph">A policy is just a PHP class with methods that return booleans. That makes it one of the easiest things in Laravel to test.</p>



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

    public function test_author_can_update_their_post(): void
    {
        $author = User::factory()->create();
        $post = Post::factory()->for($author)->create();

        $this->assertTrue($author->can('update', $post));
    }

    public function test_other_user_cannot_update_the_post(): void
    {
        $post = Post::factory()->create();
        $stranger = User::factory()->create();

        $this->assertFalse($stranger->can('update', $post));
    }
}</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">PostPolicyTest</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TestCase</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">RefreshDatabase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<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">test_author_can_update_their_post</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$author</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Post</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">for</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$author</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertTrue</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$author</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">can</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;update&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$post</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">test_other_user_cannot_update_the_post</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Post</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$stranger</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #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">$stranger</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">can</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;update&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Notice I&#8217;m using <code>$user-&gt;can()</code> instead of calling <code>new PostPolicy()</code> directly. That&#8217;s on purpose. <code>can()</code> goes through Laravel&#8217;s Gate, so the test also verifies that the policy is actually registered and that any <code>before()</code> hook behaves the way you expect. Calling the class directly would happily pass even if Laravel never found your policy.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-12">Step 2: Cover the Role Matrix with a Data Provider</h2>



<p class="wp-block-paragraph">Once you have more than two roles, writing one test per combination gets old fast. A data provider turns the whole permission matrix into a single readable table:</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>#&#91;DataProvider('deletePermissions')&#93;
public function test_delete_permissions(string $role, bool $expected): void
{
    $post = Post::factory()->create();

    $user = match ($role) {
        'author' => $post->user,
        'admin' => User::factory()->create(&#91;'is_admin' => true&#93;),
        'stranger' => User::factory()->create(),
    };

    $this->assertSame($expected, $user->can('delete', $post));
}

public static function deletePermissions(): array
{
    return [
        'author can delete' => &#91;'author', true&#93;,
        'admin can delete' => &#91;'admin', true&#93;,
        'stranger cannot delete' => &#91;'stranger', false&#93;,
    ];
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">#&#91;DataProvider(</span><span style="color: #CE9178">&#39;deletePermissions&#39;</span><span style="color: #D4D4D4">)&#93;</span></span>
<span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_delete_permissions</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$role</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">bool</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$expected</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Post</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #C586C0">match</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$role</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;author&#39;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$post</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 style="color: #CE9178">&#39;admin&#39;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;is_admin&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">&#93;),</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;stranger&#39;</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">    };</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertSame</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$expected</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">can</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;delete&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$post</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: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">deletePermissions</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">array</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> [</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;author can delete&#39;</span><span style="color: #D4D4D4"> =&gt; &#91;</span><span style="color: #CE9178">&#39;author&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;admin can delete&#39;</span><span style="color: #D4D4D4"> =&gt; &#91;</span><span style="color: #CE9178">&#39;admin&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;stranger cannot delete&#39;</span><span style="color: #D4D4D4"> =&gt; &#91;</span><span style="color: #CE9178">&#39;stranger&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">false</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">    ];</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Why strings instead of models? Data providers run before the test&#8217;s <code>setUp()</code>, so there&#8217;s no database yet. Pass a label, build the model inside the test. Small trick, saves a lot of confusion.</p>



<p class="wp-block-paragraph">When a new role appears, you add one line. When someone changes the policy, the failing case name tells you exactly what broke.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-13">Step 3: Testing Gates</h2>



<p class="wp-block-paragraph">Gates are for permissions that aren&#8217;t tied to a specific model. For example, in <code>AppServiceProvider</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(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>Gate::define('view-reports', fn (User $user) => $user->is_admin);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #4EC9B0">Gate</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">define</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;view-reports&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">fn</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">) =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">is_admin</span><span style="color: #D4D4D4">);</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Testing it is just as simple. <code>Gate::forUser()</code> lets you check any user without logging them in:</p>



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

    $this->assertTrue(Gate::forUser($admin)->allows('view-reports'));
    $this->assertTrue(Gate::forUser($user)->denies('view-reports'));
}</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_only_admins_can_view_reports</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">$admin</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;is_admin&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">true</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"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertTrue</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Gate</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">forUser</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$admin</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">allows</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;view-reports&#39;</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertTrue</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Gate</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">forUser</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">denies</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;view-reports&#39;</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Always test both sides. A gate that returns <code>true</code> for everyone passes the admin test perfectly.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-14">Step 4: Feature Tests — Is the Policy Actually Wired In?</h2>



<p class="wp-block-paragraph">This is the part that really matters.</p>



<p class="wp-block-paragraph">Your policy tests can be perfect, and the app can still be wide open. Someone deletes the <code>Gate::authorize()</code> line while refactoring the controller. All policy tests stay green. Production is now broken in the worst possible way: silently.</p>



<p class="wp-block-paragraph">The only thing that catches this is a feature test that hits the real endpoint as the <em>wrong</em> user:</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_cannot_update_someone_elses_post(): void
{
    $post = Post::factory()->create(&#91;'title' => 'Original title'&#93;);
    $stranger = User::factory()->create();

    $this->actingAs($stranger)
        ->put(route('posts.update', $post), &#91;'title' => 'Hacked'&#93;)
        ->assertForbidden();

    $this->assertSame('Original title', $post->fresh()->title);
}</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_cannot_update_someone_elses_post</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Post</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;title&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Original title&#39;</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$stranger</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">actingAs</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$stranger</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">put</span><span style="color: #D4D4D4">(</span><span style="color: #DCDCAA">route</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;posts.update&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">), &#91;</span><span style="color: #CE9178">&#39;title&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Hacked&#39;</span><span style="color: #D4D4D4">&#93;)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">assertForbidden</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertSame</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Original title&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">fresh</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #9CDCFE">title</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">That last assertion is the one people skip. Don&#8217;t. A <code>403</code> response doesn&#8217;t guarantee nothing happened. If the update ran <em>before</em> the authorization check, you&#8217;d get a 403 and a modified post. Always check that the side effect did not happen.</p>



<p class="wp-block-paragraph">Guests are a separate case, so give them a separate test:</p>



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

    $this->put(route('posts.update', $post), &#91;'title' => 'Hacked'&#93;)
        ->assertRedirect(route('login'));
}</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_guest_is_redirected_to_login</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">$post</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Post</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">put</span><span style="color: #D4D4D4">(</span><span style="color: #DCDCAA">route</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;posts.update&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">), &#91;</span><span style="color: #CE9178">&#39;title&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Hacked&#39;</span><span style="color: #D4D4D4">&#93;)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">assertRedirect</span><span style="color: #D4D4D4">(</span><span style="color: #DCDCAA">route</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;login&#39;</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">For JSON APIs, use <code>assertUnauthorized()</code> (401) for guests and <code>assertForbidden()</code> (403) for logged-in users without permission. Mixing those two up is a classic.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-15">Step 5: Testing Middleware</h2>



<p class="wp-block-paragraph">Say you have a middleware that only lets subscribed users into certain pages:</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 EnsureUserIsSubscribed
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! $request->user()?->subscribed) {
            return redirect()->route('billing');
        }

        return $next($request);
    }
}</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">EnsureUserIsSubscribed</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">Request</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">Closure</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$next</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">Response</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">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">user</span><span style="color: #D4D4D4">()?-&gt;</span><span style="color: #9CDCFE">subscribed</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">redirect</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">route</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;billing&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$next</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></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">I like to test middleware on its own using a tiny route defined only for the test. That way the test doesn&#8217;t depend on whatever controller happens to use it today:</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>protected function setUp(): void
{
    parent::setUp();

    Route::middleware(&#91;'web', EnsureUserIsSubscribed::class&#93;)
        ->get('/_test/premium', fn () => 'premium content');
}

public function test_unsubscribed_user_is_redirected_to_billing(): void
{
    $user = User::factory()->create(&#91;'subscribed' => false&#93;);

    $this->actingAs($user)
        ->get('/_test/premium')
        ->assertRedirect(route('billing'));
}

public function test_subscribed_user_gets_through(): void
{
    $user = User::factory()->create(&#91;'subscribed' => true&#93;);

    $this->actingAs($user)
        ->get('/_test/premium')
        ->assertOk()
        ->assertSee('premium content');
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">protected</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">setUp</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">parent</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">setUp</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Route</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">middleware</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;web&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">EnsureUserIsSubscribed</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">&#93;)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/_test/premium&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">fn</span><span style="color: #D4D4D4"> () =&gt; </span><span style="color: #CE9178">&#39;premium content&#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: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_unsubscribed_user_is_redirected_to_billing</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;subscribed&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">false</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">actingAs</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">        -&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/_test/premium&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">assertRedirect</span><span style="color: #D4D4D4">(</span><span style="color: #DCDCAA">route</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;billing&#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: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_subscribed_user_gets_through</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;subscribed&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #569CD6">true</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">actingAs</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">        -&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/_test/premium&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">assertOk</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">        -&gt;</span><span style="color: #DCDCAA">assertSee</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;premium content&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Then add at least one feature test on a real protected route. The isolated test proves the middleware works. The real-route test proves it&#8217;s still attached.</p>



<p class="wp-block-paragraph">One warning: be careful with <code>withoutMiddleware()</code>. It&#8217;s tempting to use it everywhere to make tests &#8220;simpler&#8221;, but it disables auth too. If half your suite runs without middleware, half your suite never tests authorization at all.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-16">Bonus: 403 or 404?</h2>



<p class="wp-block-paragraph">Sometimes you don&#8217;t want to tell a stranger that a resource even exists. If you scope the query to the current user, they get a 404 instead of a 403:</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>$post = $request->user()->posts()->findOrFail($id);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #9CDCFE">$post</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">()-&gt;</span><span style="color: #DCDCAA">posts</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">findOrFail</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$id</span><span style="color: #D4D4D4">);</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Both approaches are fine. What&#8217;s not fine is not knowing which one your app uses. Pick one, and pin it with a test (<code>assertNotFound()</code> or <code>assertForbidden()</code>), so a future refactor can&#8217;t change the behavior without anyone noticing.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-17">Quick Checklist</h2>



<p class="wp-block-paragraph">Before I consider an endpoint covered, I go through this list:</p>



<ul class="wp-block-list">
<li>At least one test where the <em>wrong</em> user tries the action and gets denied</li>



<li>The database is unchanged after a denied request</li>



<li>Guests are tested separately from logged-in users</li>



<li>Policy unit tests cover the logic; feature tests cover the wiring</li>



<li><code>withoutMiddleware()</code> is used rarely, and never in authorization tests</li>
</ul>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-18">Conclusion</h2>



<p class="wp-block-paragraph">Authorization bugs are sneaky because they don&#8217;t crash anything. The app works, the tests pass, and the only person who notices is the one who wasn&#8217;t supposed to get in.</p>



<p class="wp-block-paragraph">The good news is that these tests are some of the simplest you&#8217;ll ever write. A factory, <code>actingAs()</code>, one request, one assertion that something <em>didn&#8217;t</em> happen. Ten minutes of work per endpoint.</p>



<p class="wp-block-paragraph">So next time you add a policy, don&#8217;t stop at &#8220;the author can edit&#8221;. Write the test for the stranger too. Future you (and your security team) will thank you.</p>
<p>The post <a href="https://codecraftdiary.com/2026/10/11/testing-authorization-laravel-policies-gates-middleware/">Testing Authorization in Laravel: Policies, Gates, and Middleware</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/10/11/testing-authorization-laravel-policies-gates-middleware/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Refactoring Legacy Laravel Code in Small, Safe Steps (With Real Examples)</title>
		<link>https://codecraftdiary.com/2026/09/27/refactoring-legacy-laravel-code-small-steps/</link>
					<comments>https://codecraftdiary.com/2026/09/27/refactoring-legacy-laravel-code-small-steps/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 27 Sep 2026 12:59:12 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3394</guid>

					<description><![CDATA[<p>A few months ago, I wrote about testing legacy Laravel code without refactoring first. We took an ugly controller method, didn&#8217;t touch a single line, and wrapped it in integration tests. That article ended with a promise: once the tests exist, you can finally start cleaning up. So let&#8217;s do it. Same controller, same tests. [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/09/27/refactoring-legacy-laravel-code-small-steps/">Refactoring Legacy Laravel Code in Small, Safe Steps (With Real Examples)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A few months ago, I wrote about <a href="https://codecraftdiary.com/2025/12/13/testing-legacy-php-code-practical-strategies/">testing legacy Laravel </a><a href="https://codecraftdiary.com/2025/12/13/testing-legacy-php-code-practical-strategies/" target="_blank" rel="noreferrer noopener">code </a><a href="https://codecraftdiary.com/2025/12/13/testing-legacy-php-code-practical-strategies/">without refactoring first</a>. We took an ugly controller method, didn&#8217;t touch a single line, and wrapped it in integration tests.</p>



<p class="wp-block-paragraph">That article ended with a promise: once the tests exist, you can finally start cleaning up.</p>



<p class="wp-block-paragraph">So let&#8217;s do it. Same controller, same tests. No new architecture, no design patterns, no big rewrite. Just small moves, one at a time, with green tests after every single one.</p>



<p class="wp-block-paragraph">Previous article in this category: <a href="https://codecraftdiary.com/2026/08/30/refactoring-the-database-layer-replacing-query-scope-bloat-with-repository-query-objects/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/08/30/refactoring-the-database-layer-replacing-query-scope-bloat-with-repository-query-objects/</a></p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-24">The code we&#8217;re starting with</h3>



<p class="wp-block-paragraph">Here&#8217;s the method again, exactly as it was:</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 processOrder(Request $request)
{
    $order = new Order();
    $order->product_id = $request->product_id;
    $order->quantity = $request->quantity;
    $order->user_id = auth()->id();
    $order->status = 'pending';
    $order->save();

    // business rules mixed directly inside controller
    if ($order->quantity > 10) {
        $order->priority = 'high';
    }

    if ($order->product_id === 999) {
        // fetch external pricing
        $response = Http::post('https://external-api.com/prices', &#91;
            'product_id' => $order->product_id,
        &#93;);

        if ($response->ok()) {
            $order->external_price = $response->json('price');
        }
    }

    // logging inside business logic
    Log::info('Order processed', &#91;
        'order_id' => $order->id,
        'user' => auth()->id(),
    &#93;);

    // send email
    Mail::to(auth()->user())->send(new OrderCreatedMail($order));

    // update stock
    $product = Product::find($order->product_id);
    $product->stock -= $order->quantity;
    $product->save();

    return response()->json(&#91;
        'id' => $order->id,
        'status' => $order->status,
        'priority' => $order->priority ?? 'normal',
    &#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">processOrder</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Request</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</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: #9CDCFE">quantity</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">quantity</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: #9CDCFE">user_id</span><span style="color: #D4D4D4"> = </span><span style="color: #DCDCAA">auth</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">id</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">status</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 style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">save</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// business rules mixed directly inside controller</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">quantity</span><span style="color: #D4D4D4"> &gt; </span><span style="color: #B5CEA8">10</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">priority</span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">&#39;high&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4"> === </span><span style="color: #B5CEA8">999</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// fetch external pricing</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;https://external-api.com/prices&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">ok</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: #9CDCFE">external_price</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;price&#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: #6A9955">// logging inside business logic</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;Order processed&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;order_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">auth</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">id</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// send email</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: #DCDCAA">auth</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">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">OrderCreatedMail</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">));</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// update stock</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stock</span><span style="color: #D4D4D4"> -= </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">quantity</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">save</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>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">status</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;priority&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">priority</span><span style="color: #D4D4D4"> ?? </span><span style="color: #CE9178">&#39;normal&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><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></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">It works. Nobody wants to touch it. That&#8217;s the definition of legacy code in most teams.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-25">The only rule: small steps, green tests</h3>



<p class="wp-block-paragraph">Before we start, one rule that matters more than any technique in this article:</p>



<p class="wp-block-paragraph"><strong>Change one thing. Run the tests. Commit. Repeat.</strong></p>



<p class="wp-block-paragraph">If a step breaks something, you know exactly which step it was, and <code>git checkout .</code> costs you two minutes instead of an afternoon. It feels slow. In practice, it&#8217;s the fastest way to refactor code you don&#8217;t fully understand.</p>



<h3 class="wp-block-heading">Step 0: Pin down the behavior you&#8217;re about to touch</h3>



<p class="wp-block-paragraph">Our existing tests cover the happy path and the external price. But the priority rule isn&#8217;t tested at all, and it&#8217;s exactly the kind of thing that breaks quietly during refactoring. So before changing anything, I add one more characterization test:</p>



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

    $this->actingAs(User::factory()->create());
    $product = Product::factory()->create(&#91;'stock' => 50&#93;);

    $this->postJson('/api/orders/process', &#91;
        'product_id' => $product->id,
        'quantity' => 10,
    &#93;)->assertJson(&#91;'priority' => 'normal'&#93;);

    $this->postJson('/api/orders/process', &#91;
        'product_id' => $product->id,
        'quantity' => 11,
    &#93;)->assertJson(&#91;'priority' => 'high'&#93;);
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_priority_depends_on_quantity</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">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">actingAs</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">());</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;stock&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">50</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders/process&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">10</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;)-&gt;</span><span style="color: #DCDCAA">assertJson</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;priority&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;normal&#39;</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders/process&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">11</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;)-&gt;</span><span style="color: #DCDCAA">assertJson</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;priority&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;high&#39;</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Note the boundary. The rule is <code>&gt; 10</code>, not <code>&gt;= 10</code>, and &#8220;10 is normal&#8221; is exactly the kind of detail you&#8217;d accidentally change while &#8220;cleaning up&#8221;.</p>



<p class="wp-block-paragraph">The test passes. Commit.</p>



<h3 class="wp-block-heading">Step 1: Give magic values a name</h3>



<p class="wp-block-paragraph"><code>10</code> and <code>999</code> mean something to someone. Probably to someone who left the company in 2021.</p>



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

    // ...

    if ($order->quantity > self::HIGH_PRIORITY_QUANTITY) {
        $order->priority = 'high';
    }

    if ($order->product_id === self::EXTERNALLY_PRICED_PRODUCT_ID) {
        // ...
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderController</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Controller</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">const</span><span style="color: #D4D4D4"> HIGH_PRIORITY_QUANTITY = </span><span style="color: #B5CEA8">10</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">const</span><span style="color: #D4D4D4"> EXTERNALLY_PRICED_PRODUCT_ID = </span><span style="color: #B5CEA8">999</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>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">quantity</span><span style="color: #D4D4D4"> &gt; </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::HIGH_PRIORITY_QUANTITY) {</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: #9CDCFE">priority</span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">&#39;high&#39;</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4"> === </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::EXTERNALLY_PRICED_PRODUCT_ID) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// ...</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">This is the most boring refactoring there is, and one of the most useful. The next person reading this code no longer has to guess why product 999 is special. The name tells them.</p>



<p class="wp-block-paragraph">Tests: green. Commit.</p>



<h3 class="wp-block-heading">Step 2: Stop asking for the same thing three times</h3>



<p class="wp-block-paragraph">The method calls <code>auth()</code> three times. It&#8217;s not a performance problem, it&#8217;s a readability one: every call makes you wonder whether it could return something different.</p>



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

$order->user_id = $user->id;
// ...
Log::info('Order processed', &#91;
    'order_id' => $order->id,
    'user' => $user->id,
&#93;);

Mail::to($user)->send(new OrderCreatedMail($order));</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #DCDCAA">auth</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: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">user_id</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: #6A9955">// ...</span></span>
<span class="line"><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;Order processed&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;order_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;user&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span>
<span class="line"><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">OrderCreatedMail</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">));</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">One variable, one source of truth. Tests: green. Commit.</p>



<h3 class="wp-block-heading">Step 3: Extract methods until the main method reads like a story</h3>



<p class="wp-block-paragraph">This is the step that changes how the code <em>feels</em>. The comments in the original method are a big hint: every <code>// fetch external pricing</code> or <code>// update stock</code> is a method that wants to exist.</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 processOrder(Request $request)
{
    $user = auth()->user();

    $order = $this->createPendingOrder($request, $user);

    $this->applyPriority($order);
    $this->applyExternalPrice($order);

    Log::info('Order processed', &#91;
        'order_id' => $order->id,
        'user' => $user->id,
    &#93;);

    Mail::to($user)->send(new OrderCreatedMail($order));

    $this->decreaseStock($order);

    return response()->json(&#91;
        'id' => $order->id,
        'status' => $order->status,
        'priority' => $order->priority ?? 'normal',
    &#93;, 201);
}

private function createPendingOrder(Request $request, User $user): Order
{
    $order = new Order();
    $order->product_id = $request->product_id;
    $order->quantity = $request->quantity;
    $order->user_id = $user->id;
    $order->status = 'pending';
    $order->save();

    return $order;
}

private function applyPriority(Order $order): void
{
    if ($order->quantity > self::HIGH_PRIORITY_QUANTITY) {
        $order->priority = 'high';
    }
}

private function applyExternalPrice(Order $order): void
{
    if ($order->product_id === self::EXTERNALLY_PRICED_PRODUCT_ID) {
        $response = Http::post('https://external-api.com/prices', &#91;
            'product_id' => $order->product_id,
        &#93;);

        if ($response->ok()) {
            $order->external_price = $response->json('price');
        }
    }
}

private function decreaseStock(Order $order): void
{
    $product = Product::find($order->product_id);
    $product->stock -= $order->quantity;
    $product->save();
}</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">processOrder</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Request</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #DCDCAA">auth</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: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">createPendingOrder</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$request</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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">applyPriority</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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">applyExternalPrice</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">info</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Order processed&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;order_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">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">OrderCreatedMail</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">));</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">decreaseStock</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">status</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;priority&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">priority</span><span style="color: #D4D4D4"> ?? </span><span style="color: #CE9178">&#39;normal&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><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>
<span class="line"><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">createPendingOrder</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">User</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">Order</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</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: #9CDCFE">quantity</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">quantity</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: #9CDCFE">user_id</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: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">status</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 style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">save</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span>
<span class="line"><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">applyPriority</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">quantity</span><span style="color: #D4D4D4"> &gt; </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::HIGH_PRIORITY_QUANTITY) {</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: #9CDCFE">priority</span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">&#39;high&#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: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">applyExternalPrice</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4"> === </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::EXTERNALLY_PRICED_PRODUCT_ID) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;https://external-api.com/prices&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">ok</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: #9CDCFE">external_price</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;price&#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>
<span class="line"><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">decreaseStock</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">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">find</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stock</span><span style="color: #D4D4D4"> -= </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">quantity</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">save</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Two things worth noticing.</p>



<p class="wp-block-paragraph">First, the comments are gone, and nothing is lost. The method names say the same thing, and unlike comments, method names don&#8217;t go out of date when someone changes the code.</p>



<p class="wp-block-paragraph">Second, I kept the order of operations <strong>exactly</strong> the same. It&#8217;s tempting to &#8220;tidy up&#8221; the sequence while you&#8217;re in there. Don&#8217;t. The order of side effects is behavior, and we&#8217;re not changing behavior yet.</p>



<p class="wp-block-paragraph">Tests: green. Commit. (Honestly, this could be three commits, one per extracted method. Smaller is better.)</p>



<h3 class="wp-block-heading">Step 4: Flatten with guard clauses</h3>



<p class="wp-block-paragraph"><code>applyExternalPrice</code> has two levels of nesting. Guard clauses turn &#8220;if this, then if that, then do the thing&#8221; into &#8220;if not this, leave&#8221;:</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>private function applyExternalPrice(Order $order): void
{
    if ($order->product_id !== self::EXTERNALLY_PRICED_PRODUCT_ID) {
        return;
    }

    $priceResponse = Http::post('https://external-api.com/prices', &#91;
        'product_id' => $order->product_id,
    &#93;);

    if (! $priceResponse->ok()) {
        return;
    }

    $order->external_price = $priceResponse->json('price');
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">applyExternalPrice</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4"> !== </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::EXTERNALLY_PRICED_PRODUCT_ID) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$priceResponse</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;https://external-api.com/prices&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">product_id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (! </span><span style="color: #9CDCFE">$priceResponse</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">ok</span><span style="color: #D4D4D4">()) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">external_price</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$priceResponse</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;price&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">I also snuck in a rename: <code>$response</code> became <code>$priceResponse</code>. In a controller that also returns an HTTP response, a variable called <code>$response</code> holding the response of a <em>different</em> HTTP call is a small trap. Renaming costs nothing and removes it.</p>



<p class="wp-block-paragraph">Tests: green. Commit.</p>



<h3 class="wp-block-heading">Step 5: The bug you&#8217;ll find along the way</h3>



<p class="wp-block-paragraph">Now look at the main method again, the way it reads after Step 3:</p>



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

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



<p class="wp-block-paragraph">In the original 40-line method, this was easy to miss. Now it jumps out: <strong>the order is saved before priority and external price are set, and it&#8217;s never saved again.</strong></p>



<p class="wp-block-paragraph">That means <code>priority</code> and <code>external_price</code> never reach the database. The API response still says <code>"priority": "high"</code>, because it reads the value from memory, not from the database. So the client is told one thing, and the database stores another.</p>



<p class="wp-block-paragraph">This is the part nobody tells you about refactoring: making code readable is also how you find the bugs that were hiding in it.</p>



<p class="wp-block-paragraph">And here&#8217;s the rule that matters: <strong>don&#8217;t fix it in the same commit.</strong> Everything up to now was a pure refactoring, with no behavior change. Fixing the bug <em>is</em> a behavior change. Keep them separate, so that if something downstream turns out to depend on the old behavior (it happens more often than you&#8217;d think), you can revert just the fix.</p>



<p class="wp-block-paragraph">Before fixing, it&#8217;s worth a quick check with whoever owns this feature. Is there a report that treats <code>null</code> priority as normal? An admin screen that shows the priority? Five minutes of asking beats a surprised product manager.</p>



<p class="wp-block-paragraph">Then the fix goes the proper way: red test first.</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_large_orders_are_stored_with_high_priority()
{
    Mail::fake();
    Http::fake();

    $this->actingAs(User::factory()->create());
    $product = Product::factory()->create(&#91;'stock' => 50&#93;);

    $this->postJson('/api/orders/process', &#91;
        'product_id' => $product->id,
        'quantity' => 11,
    &#93;);

    $this->assertDatabaseHas('orders', &#91;
        'product_id' => $product->id,
        'priority' => 'high',
    &#93;);
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_large_orders_are_stored_with_high_priority</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">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">actingAs</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">());</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;stock&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">50</span><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders/process&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">11</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;priority&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;high&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">It fails. Good, that proves the bug is real. Now the fix, which after our refactoring is almost trivial: build the order first, save it once at the end.</p>



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

$this->applyPriority($order);
$this->applyExternalPrice($order);

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



<p class="wp-block-paragraph">Test: green. Commit, with a message that says what changed and why: <code>Fix: persist order priority and external price</code>.</p>



<p class="wp-block-paragraph">Try making this fix in the original method. It&#8217;s doable, but you&#8217;d be moving a <code>save()</code> call around inside a wall of mixed concerns and hoping nothing else depended on it. After the refactoring, the fix is three lines and obviously correct.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-26">Where to stop</h3>



<p class="wp-block-paragraph">The controller is still doing too much. It&#8217;s creating orders, calling an external API, sending email and managing stock. Stock updates aren&#8217;t atomic, and <code>Product::find()</code> can return <code>null</code>.</p>



<p class="wp-block-paragraph">All true. None of it needs to be fixed today.</p>



<p class="wp-block-paragraph">This is the point where you could move the logic into an action class (<a href="https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/">the fat controller article</a> walks through that), or wrap the stock update in a transaction. But each of those is its own change, with its own tests and its own commit. Refactoring doesn&#8217;t have to end with a pattern. Sometimes the right move is to leave the code clearly better than you found it and go ship the feature you actually came here for.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-27">Before and after</h3>



<p class="wp-block-paragraph">We started with one 40-line method full of comments explaining what it does. In the end, the main method reads top to bottom in ten seconds, and four small private methods with honest names do the actual work. In addition, the magic numbers are gone, a real bug is fixed, and the test suite grew from two tests to four. Above all, we didn&#8217;t need a single new class, interface or pattern.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-28">A checklist for your next legacy refactoring</h3>



<ol class="wp-block-list">
<li>Make sure the behavior you&#8217;re touching is covered by tests. If it isn&#8217;t, write characterization tests first.</li>



<li>Replace magic numbers and strings with named constants.</li>



<li>Pull repeated calls into a single variable.</li>



<li>Turn comments into method names by extracting methods.</li>



<li>Flatten nested conditions with guard clauses.</li>



<li>Rename anything that could mislead the next reader.</li>



<li>When you find a bug, write it down, finish the refactoring, then fix it in a separate commit, test first.</li>



<li>Run the tests after every step. Commit after every step.</li>



<li>Stop when the code is clearly better, not when it&#8217;s perfect.</li>
</ol>



<p class="wp-block-paragraph">Legacy code rarely gets fixed in one heroic pull request. It gets fixed like this: one boring, safe, green step at a time.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/09/27/refactoring-legacy-laravel-code-small-steps/">Refactoring Legacy Laravel Code in Small, Safe Steps (With Real Examples)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/09/27/refactoring-legacy-laravel-code-small-steps/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The Feature Flag Graveyard: How a Safety Net Becomes Technical Debt</title>
		<link>https://codecraftdiary.com/2026/09/13/feature-flag-graveyard-technical-debt/</link>
					<comments>https://codecraftdiary.com/2026/09/13/feature-flag-graveyard-technical-debt/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 13 Sep 2026 14:00:00 +0000</pubDate>
				<category><![CDATA[Development Workflow & Best Practices]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3369</guid>

					<description><![CDATA[<p>Every engineering team starts its feature flag journey with pure intentions. You read about Trunk-Based Development, continuous deployment, and decoupled releases. You wrap your new endpoint in an if (flags.isEnabled("new-checkout-v2")) block, merge it to main on a Tuesday morning, and release it to 5% of users. It works effortlessly. Rollbacks are no longer terrifying deploy [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/09/13/feature-flag-graveyard-technical-debt/">The Feature Flag Graveyard: How a Safety Net Becomes Technical Debt</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Every engineering team starts its feature flag journey with pure intentions.</p>



<p class="wp-block-paragraph">You read about Trunk-Based Development, continuous deployment, and decoupled releases. You wrap your new endpoint in an <code>if (flags.isEnabled("new-checkout-v2"))</code> block, merge it to main on a Tuesday morning, and release it to 5% of users. It works effortlessly. Rollbacks are no longer terrifying deploy pipelines; they are a simple toggle in a UI. You feel like an engineering deity.</p>



<p class="wp-block-paragraph">Fast-forward twelve months.</p>



<p class="wp-block-paragraph">Your codebase contains 214 feature flags. Nobody remembers who created <code>enable-legacy-tax-calc-fix-final</code>, whether <code>temp-user-profile-v3</code> is fully rolled out, or why turning off <code>test-dark-mode-override</code> breaks authentication in the staging environment.</p>



<p class="wp-block-paragraph">You haven&#8217;t built a modern, resilient continuous delivery system. You’ve built a dynamic, non-deterministic branching maze—a <strong>Feature Flag Graveyard</strong>.</p>



<pre class="wp-block-preformatted"> <code>         ┌──────────────────────────┐
          │   Feature Flag Created   │
          └────────────┬─────────────┘
                       │
          ┌────────────▼─────────────┐
          │  100% Rolled Out to Prod │
          └────────────┬─────────────┘
                       │
       ┌───────────────┴───────────────┐
       │                               │
┌──────▼──────┐                ┌───────▼───────┐
│ Cleaned Up  │                │   FORGOTTEN   │
└─────────────┘                └───────┬───────┘
                                       │
                         ┌─────────────▼──────────────┐
                         │ Dead Code, Dead Tests,     │
                         │ Exponential Combinatorics  │
                         └────────────────────────────┘
</code></pre>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-32">The Hidden Cost of &#8220;Toggle Bloat&#8221;</h2>



<p class="wp-block-paragraph">When teams talk about technical debt, they usually point to outdated libraries, missing unit tests, or monolithic classes. Feature flag debt is far more insidious because it masquerades as risk mitigation.</p>



<p class="wp-block-paragraph">Here is what actually happens when flags are left to rot:</p>



<h3 class="wp-block-heading">1. Exponential Combinatorial Explosion</h3>



<p class="wp-block-paragraph">If your order processing service has 10 active feature flags, you don&#8217;t have one code path to test. You have $2^{10} = 1,024$ potential system states running in production simultaneously.</p>



<p class="wp-block-paragraph">No QA team or automated pipeline tests 1,000 combinations of code paths. Inevitably, a customer encounters a combination of flag states that no engineer has ever executed locally.</p>



<h3 class="wp-block-heading">2. Cognitive Overhead &amp; Architectural Degradation</h3>



<p class="wp-block-paragraph">Consider a simple backend service method after two years of uncleaned flags:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(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>// A legacy nightmare hiding behind modern tooling
public async processPayment(order: Order): Promise&lt;PaymentResult> {
  if (this.flags.isEnabled("use-stripe-v2")) {
    if (this.flags.isEnabled("bypass-fraud-check-beta")) {
      return this.stripeService.chargeFast(order);
    }
    return this.stripeService.chargeWithFraudCheck(order);
  } else {
    // Is anyone still hitting this branch? 
    // Is the legacy gateway even configured in production environment variables?
    return this.legacyGateway.charge(order);
  }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #6A9955">// A legacy nightmare hiding behind modern tooling</span></span>
<span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> async </span><span style="color: #DCDCAA">processPayment</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">order</span><span style="color: #D4D4D4">: Order): Promise&lt;PaymentResult&gt; {</span></span>
<span class="line"><span style="color: #D4D4D4">  </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (this</span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4">flags</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">isEnabled</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;use-stripe-v2&quot;</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"> (this</span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4">flags</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">isEnabled</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;bypass-fraud-check-beta&quot;</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"> this</span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4">stripeService</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">chargeFast</span><span style="color: #D4D4D4">(order);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> this</span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4">stripeService</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">chargeWithFraudCheck</span><span style="color: #D4D4D4">(order);</span></span>
<span class="line"><span style="color: #D4D4D4">  } </span><span style="color: #C586C0">else</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// Is anyone still hitting this branch? </span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// Is the legacy gateway even configured in production environment variables?</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> this</span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4">legacyGateway</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">charge</span><span style="color: #D4D4D4">(order);</span></span>
<span class="line"><span style="color: #D4D4D4">  }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<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">When a developer comes to refactor <code>processPayment</code>, they spend 80% of their time archaeology-ing flags: <em>Can I delete this else block? Who owns <code>use-stripe-v2</code>? Is it 100% enabled in production?</em></p>



<p class="wp-block-paragraph">Because nobody wants to break production, nobody touches it. The dead code remains forever.</p>



<h3 class="wp-block-heading">3. The Knight Capital Disaster (At Micro-Scale)</h3>



<p class="wp-block-paragraph">In 2012, Knight Capital lost $440 million in 45 minutes due to a failed software deployment. The root cause? A repurposed feature flag.</p>



<p class="wp-block-paragraph">They deployed new code that reused an old flag name, turning on obsolete, untested code on one of their servers. While your backend service might not lose half a billion dollars, executing a forgotten code path because someone toggled an abandoned flag in LaunchDarkly or Unleash will ruin your weekend just as effectively.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-33">The Core Rule: A Feature Flag Is an Unpaid Loan</h2>



<p class="wp-block-paragraph">A feature flag is not a permanent architecture design pattern. <strong>It is financial debt with compounding interest.</strong></p>



<pre class="wp-block-code"><code>Total Cost = Cost of Feature + Cost of Flag Cleanup
</code></pre>



<p class="wp-block-paragraph">If you don&#8217;t budget time to remove the flag, you haven&#8217;t finished building the feature. You have merely pushed the labor cost onto future developers—usually with interest.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-34">How to Prevent Your Codebase From Becoming a Graveyard</h2>



<p class="wp-block-paragraph">Solving flag debt isn&#8217;t a technical problem; it&#8217;s an operational discipline problem. Here are four practical patterns high-performing teams use to keep their flag count low:</p>



<h3 class="wp-block-heading">1. Classify Flags by Lifetime</h3>



<p class="wp-block-paragraph">Not all flags are created equal. Treat them according to their actual intent:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>Flag Type</strong></td><td><strong>Lifetime</strong></td><td><strong>Purpose</strong></td></tr></thead><tbody><tr><td><strong>Release Flags</strong></td><td>1–4 weeks</td><td>Short-lived safety wrappers for Trunk-Based Development. Must be deleted post-rollout.</td></tr><tr><td><strong>Experiment Flags</strong></td><td>2–8 weeks</td><td>A/B testing. Deleted immediately after statistical significance is reached.</td></tr><tr><td><strong>Ops / Kill Switches</strong></td><td>Permanent</td><td>Circuit breakers for infrastructure (e.g., <code>disable-heavy-search-indexing</code>).</td></tr><tr><td><strong>Permission Flags</strong></td><td>Permanent</td><td>Entitlements and tenant configuration (e.g., <code>enable-enterprise-sso</code>).</td></tr></tbody></table></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Rule:</strong> 90% of your flags should be <strong>Release Flags</strong>. If a flag is meant for release safety, set an explicit expiration date on day one.</p>
</blockquote>



<h3 class="wp-block-heading">2. Make Flags Expire in CI/CD</h3>



<p class="wp-block-paragraph">Don&#8217;t rely on human memory to clean up code. Use your build pipeline to enforce cleanup:</p>



<ul class="wp-block-list">
<li><strong>Stale Flag Alerts:</strong> Configure your flag management tool (Unleash, LaunchDarkly, ConfigCat) to notify Slack when a flag has been at 100% roll-out for more than 14 days.</li>



<li><strong>Failing Build Triggers:</strong> Some teams add metadata to flags in code:TypeScript<code>// @FeatureFlag(owner: "team-checkout", expires: "2026-10-15")</code> If a build runs past the expiration date and the flag is still in the codebase, the CI pipeline fails.</li>
</ul>



<h3 class="wp-block-heading">3. Pair Flag Creation with Cleanup Tickets</h3>



<p class="wp-block-paragraph">Whenever a developer opens a PR that introduces a new Release Flag, the definition of &#8220;Done&#8221; must require two artifacts:</p>



<ol start="1" class="wp-block-list">
<li>The Jira/GitHub issue for the feature implementation.</li>



<li>A scheduled, prioritized <strong>cleanup ticket</strong> to delete the flag logic and test branches 2 weeks post-launch.</li>
</ol>



<h3 class="wp-block-heading">4. Continuous Refactoring Culture</h3>



<p class="wp-block-paragraph">Make flag removal a lightweight, ongoing task rather than a quarterly &#8220;hackathon&#8221; debt-clearing chore. Removing a fully rolled-out feature flag is usually a 5-minute task: delete the <code>if/else</code>, remove the obsolete test cases, and delete the key from your flag dashboard.</p>



<h2 class="wp-block-heading">Conclusion: Clean Code Means Removing the Scaffolding</h2>



<p class="wp-block-paragraph">When builders construct a skyscraper, they use scaffolding to support the structure during assembly. Once the building stands on its own, they remove the scaffolding. Nobody leaves steel frameworks blocking the front entrance of a finished building.</p>



<p class="wp-block-paragraph">Feature flags are modern software scaffolding. They allow us to move fast, test in production, and deploy without fear. But once your feature is live and stable, tear down the scaffolding.</p>



<p class="wp-block-paragraph">Your future self—and your entire engineering team—will thank you.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/09/13/feature-flag-graveyard-technical-debt/">The Feature Flag Graveyard: How a Safety Net Becomes Technical Debt</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/09/13/feature-flag-graveyard-technical-debt/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Testing Time and Scheduling in Laravel: Travel, Carbon Freeze, and Cron Tasks</title>
		<link>https://codecraftdiary.com/2026/09/06/testing-time-in-laravel/</link>
					<comments>https://codecraftdiary.com/2026/09/06/testing-time-in-laravel/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 06 Sep 2026 14:12:00 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3362</guid>

					<description><![CDATA[<p>Time is one of the most unpredictable variables in software development. Applications regularly rely on time-sensitive logic: trial periods expire, subscription invoices process on the first of the month, verification tokens become invalid after fifteen minutes, and scheduled background tasks run during low-traffic hours. When developers test time-dependent code, a common instinct is to use [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/09/06/testing-time-in-laravel/">Testing Time and Scheduling in Laravel: Travel, Carbon Freeze, and Cron Tasks</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Time is one of the most unpredictable variables in software development. Applications regularly rely on time-sensitive logic: trial periods expire, subscription invoices process on the first of the month, verification tokens become invalid after fifteen minutes, and scheduled background tasks run during low-traffic hours.</p>



<p class="wp-block-paragraph">When developers test time-dependent code, a common instinct is to use delay functions like <code>sleep()</code>. However, delaying test execution slows down Continuous Integration (CI) pipelines and introduces flakiness.</p>



<p class="wp-block-paragraph">Testing time-based logic in PHP and Laravel does not require slowing down your test suite or manually altering system clocks. Laravel provides clear, expressive utilities to manipulate time, freeze moments, and verify scheduled tasks with precision.</p>



<pre class="wp-block-code"><code>+-----------------------------------------------------------------------+
|                         TIME-TESTING TOOLKIT                          |
+-----------------------------------------------------------------------+
|  Laravel Time Travel   --&gt;  Jump forward or backward in time          |
|  Carbon Freeze         --&gt;  Lock the clock at a specific moment       |
|  Schedule Testing      --&gt;  Assert console commands fire correctly    |
+-----------------------------------------------------------------------+
</code></pre>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-45">Why Time-Dependent Tests Fail (And How to Fix Them)</h2>



<p class="wp-block-paragraph">Testing time logic without dedicated tools typically leads to two major issues:</p>



<ol start="1" class="wp-block-list">
<li><strong>Test Suite Sluggishness:</strong> Using <code>sleep(5)</code> to test a five-second expiration window forces your test runner to pause. Accumulated across a growing test suite, these delays significantly increase build times.</li>



<li><strong>Intermittent Failures (Flakiness):</strong> Asserting that an action occurred <em>exactly</em> at <code>now()</code> can fail if CPU execution delays the assertion by even a few milliseconds.</li>
</ol>



<p class="wp-block-paragraph">To keep tests reliable and fast, tests must treat time as a controllable variable rather than an absolute constant.</p>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-46">Controlling Time with Laravel’s Travel Helpers</h2>



<p class="wp-block-paragraph">Laravel includes built-in time manipulation helpers accessible directly within test cases. Under the hood, these helpers interact with the <code>Carbon</code> library to modify the application’s perceived current time.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-47">Traveling Into the Future</h3>



<p class="wp-block-paragraph">Consider a user model with a trial period expiring 14 days after registration:</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\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Carbon;

class User extends Authenticatable
{
    public function hasActiveTrial(): bool
    {
        if ($this->trial_ends_at === null) {
            return false;
        }

        return Carbon::now()->lt($this->trial_ends_at);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Models</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\Foundation\Auth\</span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">as</span><span style="color: #D4D4D4"> Authenticatable;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Support\</span><span style="color: #4EC9B0">Carbon</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">User</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Authenticatable</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">hasActiveTrial</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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">trial_ends_at</span><span style="color: #D4D4D4"> === </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #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: #4EC9B0">Carbon</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">lt</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">trial_ends_at</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">To test whether the trial expires correctly after 14 days without waiting, use the <code>$this-&gt;travel()</code> helper:</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 Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class TrialExpirationTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_trial_expires_after_fourteen_days(): void
    {
        $user = User::factory()->create(&#91;
            'trial_ends_at' => now()->addDays(14),
        &#93;);

        // Assert trial is active initially
        $this->assertTrue($user->hasActiveTrial());

        // Travel 15 days into the future
        $this->travel(15)->days();

        // Assert trial is now expired
        $this->assertFalse($user->hasActiveTrial());
    }
}
</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">Tests\Feature</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\Foundation\Testing\</span><span style="color: #4EC9B0">RefreshDatabase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Tests\</span><span style="color: #4EC9B0">TestCase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TrialExpirationTest</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TestCase</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">RefreshDatabase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<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">test_user_trial_expires_after_fourteen_days</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;trial_ends_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">addDays</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">14</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Assert trial is active initially</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertTrue</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">hasActiveTrial</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Travel 15 days into the future</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">travel</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">15</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">days</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Assert trial is now expired</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertFalse</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">hasActiveTrial</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 has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-48">Returning to Present Time</h3>



<p class="wp-block-paragraph">When manipulating time inside a test, restoring the original state prevents unexpected side effects in subsequent tests. Laravel automatically resets time between tests, but you can also manually restore it within a test block using <code>$this-&gt;travelBack()</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(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$this->travel(2)->hours();

// Perform assertions during modified time state...

$this->travelBack(); // Restores time to the actual current moment
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">travel</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">2</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">hours</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6A9955">// Perform assertions during modified time state...</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">travelBack</span><span style="color: #D4D4D4">(); </span><span style="color: #6A9955">// Restores time to the actual current moment</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-49">Freezing Time for Deterministic Assertions</h2>



<p class="wp-block-paragraph">In certain scenarios, moving forward relative to the current clock is insufficient. When tests require checking exact timestamp matching—such as <code>created_at</code> or <code>published_at</code> fields—freezing time at an explicit instance ensures complete determinism.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-50">Using <code>travelTo()</code> and <code>Carbon::freeze()</code></h3>



<p class="wp-block-paragraph">The <code>$this-&gt;travelTo()</code> method sets the application clock to a specific fixed point:</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_post_publishes_with_exact_timestamp(): void
{
    $knownDate = now()->setDate(2026, 5, 10)->setTime(14, 0, 0);

    // Freeze time at May 10, 2026 14:00:00
    $this->travelTo($knownDate);

    $post = \App\Models\Post::create(&#91;
        'title' => 'Testing Time in Laravel',
        'published_at' => now(),
    &#93;);

    $this->assertEquals('2026-05-10 14:00:00', $post->published_at->format('Y-m-d H:i:s'));
}
</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_post_publishes_with_exact_timestamp</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">$knownDate</span><span style="color: #D4D4D4"> = </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">setDate</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">2026</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">10</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">setTime</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">14</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">0</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// Freeze time at May 10, 2026 14:00:00</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">travelTo</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$knownDate</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4"> = \App\Models\</span><span style="color: #4EC9B0">Post</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;title&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;Testing Time in Laravel&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;published_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertEquals</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;2026-05-10 14:00:00&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$post</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">published_at</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">format</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Y-m-d H:i:s&#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">Alternatively, if you are working outside a standard Laravel <code>TestCase</code> or in a standalone PHP context, you can interact directly with Carbon&#8217;s underlying freeze method:</p>



<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>use Illuminate\Support\Carbon;

// Freeze clock at current moment
Carbon::setTestNow(now());

// Execute code requiring frozen time...

// Clear test time when finished
Carbon::setTestNow(null);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Support\</span><span style="color: #4EC9B0">Carbon</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6A9955">// Freeze clock at current moment</span></span>
<span class="line"><span style="color: #4EC9B0">Carbon</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">setTestNow</span><span style="color: #D4D4D4">(</span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6A9955">// Execute code requiring frozen time...</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6A9955">// Clear test time when finished</span></span>
<span class="line"><span style="color: #4EC9B0">Carbon</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">setTestNow</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">null</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>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-51">Scoped Time Manipulation with <code>travelTo</code> Closures</h2>



<p class="wp-block-paragraph">To ensure time manipulation is scoped strictly to a specific block of logic, pass a callback closure to <code>travelTo()</code>. This automatically restores the standard flow of time as soon as the closure finishes executing:</p>



<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_temporary_download_link_invalidation(): void
{
    $link = \App\Services\DownloadService::generateLink();

    $this->assertTrue($link->isValid());

    // Execute logic strictly 30 minutes in the future
    $this->travel(30)->minutes(function () use ($link) {
        $this->assertFalse($link->isValid());
    });

    // Time is automatically restored here
    $this->assertTrue(now()->isToday());
}
</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_temporary_download_link_invalidation</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">$link</span><span style="color: #D4D4D4"> = \App\Services\</span><span style="color: #4EC9B0">DownloadService</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">generateLink</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">assertTrue</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$link</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isValid</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #6A9955">// Execute logic strictly 30 minutes in the future</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">travel</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">30</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">minutes</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> () </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$link</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertFalse</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$link</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isValid</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">// Time is automatically restored here</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertTrue</span><span style="color: #D4D4D4">(</span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">isToday</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-52">Testing Laravel Scheduled Tasks (Cron Jobs)</h2>



<p class="wp-block-paragraph">Managing scheduled tasks via <code>app/Console/Kernel.php</code> or <code>routes/console.php</code> is standard practice in Laravel applications. Verifying that scheduled commands execute at the correct time intervals should not require running full system cron daemons during testing.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-53">Testing Schedule Execution with <code>Event::fake()</code> and Console Utilities</h3>



<p class="wp-block-paragraph">Suppose a clean-up task is scheduled to run daily at midnight:</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>// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('prune:old-tokens')->dailyAt('00:00');
</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">// routes/console.php</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Support\Facades\</span><span style="color: #4EC9B0">Schedule</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #4EC9B0">Schedule</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">command</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;prune:old-tokens&#39;</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">dailyAt</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;00:00&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">To test that this command fires when midnight arrives, combine time travel with the <code>Schedule</code> facade assertions:</p>



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

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ScheduleTest extends TestCase
{
    use RefreshDatabase;

    public function test_prune_tokens_command_is_scheduled_daily_at_midnight(): void
    {
        $schedule = $this->app->make(Schedule::class);

        // Freeze time at 23:59:00
        $this->travelTo(now()->setTime(23, 59, 0));

        $eventsAt2359 = collect($schedule->events())->filter(function ($event) {
            return $event->isDue($this->app);
        });

        // Command should NOT be due at 23:59
        $this->assertFalse(
            $eventsAt2359->contains(fn ($event) => str_contains($event->command, 'prune:old-tokens'))
        );

        // Travel 1 minute forward to 00:00
        $this->travel(1)->minute();

        $eventsAtMidnight = collect($schedule->events())->filter(function ($event) {
            return $event->isDue($this->app);
        });

        // Command SHOULD be due at 00:00
        $this->assertTrue(
            $eventsAtMidnight->contains(fn ($event) => str_contains($event->command, 'prune:old-tokens'))
        );
    }
}
</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">Tests\Feature</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\Console\Scheduling\</span><span style="color: #4EC9B0">Schedule</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Foundation\Testing\</span><span style="color: #4EC9B0">RefreshDatabase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Tests\</span><span style="color: #4EC9B0">TestCase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ScheduleTest</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TestCase</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">RefreshDatabase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<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">test_prune_tokens_command_is_scheduled_daily_at_midnight</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">$schedule</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">app</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">make</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Schedule</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Freeze time at 23:59:00</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">travelTo</span><span style="color: #D4D4D4">(</span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">setTime</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">23</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">59</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">0</span><span style="color: #D4D4D4">));</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$eventsAt2359</span><span style="color: #D4D4D4"> = </span><span style="color: #DCDCAA">collect</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$schedule</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">events</span><span style="color: #D4D4D4">())-&gt;</span><span style="color: #DCDCAA">filter</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">function</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 style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$event</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isDue</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">app</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">// Command should NOT be due at 23:59</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertFalse</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$eventsAt2359</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">contains</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">fn</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$event</span><span style="color: #D4D4D4">) =&gt; </span><span style="color: #DCDCAA">str_contains</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$event</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">command</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;prune:old-tokens&#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: #6A9955">// Travel 1 minute forward to 00:00</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">travel</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">minute</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$eventsAtMidnight</span><span style="color: #D4D4D4"> = </span><span style="color: #DCDCAA">collect</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$schedule</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">events</span><span style="color: #D4D4D4">())-&gt;</span><span style="color: #DCDCAA">filter</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">function</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 style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$event</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isDue</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">app</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">// Command SHOULD be due at 00:00</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertTrue</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$eventsAtMidnight</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">contains</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">fn</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$event</span><span style="color: #D4D4D4">) =&gt; </span><span style="color: #DCDCAA">str_contains</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$event</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">command</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;prune:old-tokens&#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>



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-54">Key Best Practices for Time Testing</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>Practice</strong></td><td><strong>Details</strong></td><td><strong>Benefit</strong></td></tr></thead><tbody><tr><td><strong>Avoid Delays</strong></td><td>Replace <code>sleep()</code> and <code>usleep()</code> with <code>$this-&gt;travel()</code></td><td>Prevents slow CI test suites</td></tr><tr><td><strong>Clear Mocked Time</strong></td><td>Rely on Laravel&#8217;s automatic cleanup or use <code>$this-&gt;travelBack()</code></td><td>Prevents leaking altered time across tests</td></tr><tr><td><strong>Use Explicit Instances</strong></td><td>Freeze time with exact dates (<code>travelTo()</code>) for strict comparisons</td><td>Eliminates edge-case timestamp mismatch failures</td></tr><tr><td><strong>Test Boundary Conditions</strong></td><td>Test 1 second before, exactly at, and 1 second after an expiration point</td><td>Ensures accurate condition boundaries</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">Time-dependent logic does not have to introduce unpredictability into your application&#8217;s test suite. By utilizing Laravel&#8217;s built-in time travel utilities, freezing Carbon instances, and evaluating scheduled events directly, you can write fast, reliable, and deterministic tests for any time-sensitive feature.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/09/06/testing-time-in-laravel/">Testing Time and Scheduling in Laravel: Travel, Carbon Freeze, and Cron Tasks</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/09/06/testing-time-in-laravel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Refactoring the Database Layer: Replacing Query Scope Bloat with Repository/Query Objects</title>
		<link>https://codecraftdiary.com/2026/08/30/refactoring-the-database-layer-replacing-query-scope-bloat-with-repository-query-objects/</link>
					<comments>https://codecraftdiary.com/2026/08/30/refactoring-the-database-layer-replacing-query-scope-bloat-with-repository-query-objects/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sun, 30 Aug 2026 13:15:00 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3359</guid>

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



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



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



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



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



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



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



<p class="wp-block-paragraph">The path of least resistance is usually adding a scope directly to the <code>Customer</code> model:</p>



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

class Customer extends Model
{
    public function scopeDormantHighValueB2b($query, int $daysThreshold = 90)
    {
        return $query->where('type', 'b2b')
            ->whereHas('subscriptions', function ($q) {
                $q->where('status', 'active');
            })
            ->whereDoesntHave('orders', function ($q) use ($daysThreshold) {
                $q->where('created_at', '>=', now()->subDays($daysThreshold));
            })
            ->having('total_lifetime_spend', '>', 10000);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #6A9955">// App\Models\Customer.php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Customer</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Model</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">scopeDormantHighValueB2b</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$daysThreshold</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">90</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;type&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;b2b&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">whereHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;subscriptions&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;active&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">            })</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">whereDoesntHave</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$daysThreshold</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;created_at&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;&gt;=&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">subDays</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$daysThreshold</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">            })</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">having</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;total_lifetime_spend&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;&gt;&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">10000</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">It works. It&#8217;s expressive enough in the controller: <code>Customer::dormantHighValueB2b()-&gt;get()</code>. But consider the structural consequences over time:</p>



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



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



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



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



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



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



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



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



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



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

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

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

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

    private function getBaseQuery(): Builder
    {
        return Customer::query()
            ->where('type', 'b2b')
            ->whereHas('subscriptions', fn (Builder $q) => $q->where('status', 'active'))
            ->whereDoesntHave('orders', fn (Builder $q) => 
                $q->where('created_at', '>=', now()->subDays($this->daysThreshold))
            )
            ->where('total_lifetime_spend', '>=', $this->minimumSpend);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Queries</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Models\</span><span style="color: #4EC9B0">Customer</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Contracts\Pagination\</span><span style="color: #4EC9B0">LengthAwarePaginator</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Database\Eloquent\</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">final</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">GetDormantHighValueB2bCustomersQuery</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$daysThreshold</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">90</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$minimumSpend</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">10000</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$perPage</span><span style="color: #D4D4D4"> = </span><span style="color: #B5CEA8">25</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">LengthAwarePaginator</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">getBaseQuery</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">paginate</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$perPage</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">getBaseQuery</span><span style="color: #D4D4D4">(): </span><span style="color: #4EC9B0">Builder</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Customer</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">query</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;type&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;b2b&#39;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">whereHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;subscriptions&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">fn</span><span style="color: #D4D4D4"> (</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) =&gt; </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;active&#39;</span><span style="color: #D4D4D4">))</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">whereDoesntHave</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">fn</span><span style="color: #D4D4D4"> (</span><span style="color: #4EC9B0">Builder</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">) =&gt; </span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #9CDCFE">$q</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;created_at&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;&gt;=&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">subDays</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">daysThreshold</span><span style="color: #D4D4D4">))</span></span>
<span class="line"><span style="color: #D4D4D4">            )</span></span>
<span class="line"><span style="color: #D4D4D4">            -&gt;</span><span style="color: #DCDCAA">where</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;total_lifetime_spend&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;&gt;=&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">minimumSpend</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Notice what changed here:</p>



<ul class="wp-block-list">
<li><strong>Strict Encapsulation:</strong> The internal query construction (<code>buildQuery()</code>) is kept <code>private</code>. The outside world only receives the hydrated result (<code>LengthAwarePaginator</code>) via <code>handle()</code>, preventing callers from chaining arbitrary SQL downstream.</li>
</ul>



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



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



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



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



<p class="wp-block-paragraph">Now, inside your controller, job, or CLI command, you simply inject or instantiate the Query Object:</p>



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

use App\Queries\GetDormantHighValueB2bCustomersQuery;
use Illuminate\Http\JsonResponse;

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

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

        return response()->json($customers);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Http\Controllers</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Queries\</span><span style="color: #4EC9B0">GetDormantHighValueB2bCustomersQuery</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Http\</span><span style="color: #4EC9B0">JsonResponse</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">MarketingTargetController</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Controller</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">index</span><span style="color: #D4D4D4">(): </span><span style="color: #4EC9B0">JsonResponse</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">GetDormantHighValueB2bCustomersQuery</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            daysThreshold: </span><span style="color: #B5CEA8">60</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            minimumSpend: </span><span style="color: #B5CEA8">15000</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$customers</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$query</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">perPage</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">50</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">json</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$customers</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">One of the easiest ways to make a test frustrating is filling it with dozens of unrelated assertions.</p>



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

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



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



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



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



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



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



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



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



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



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



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



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Unfortunately, many tests quietly depend on hidden data.</p>



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



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



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



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



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



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



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



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



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Instead of relying on unknown database state, create exactly what your test needs.</p>



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



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



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



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



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



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



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



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Imagine testing an API response.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$this->assertEquals(
    $expectedJson,
    $response->getContent()
);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_complete_checkout_process</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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



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



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



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



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



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$response->assertJsonFragment(&#091;
    'email' => 'john@example.com',
&#093;);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_complete_checkout_process</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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



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



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



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



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Open a large test suite and sometimes every test starts with exactly the same setup.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$user = User::factory()->create();
$account = Account::factory()->create();
$order = Order::factory()->create();
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_complete_checkout_process</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Product</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">postJson</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/api/orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertCreated</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Mail</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertSent</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderConfirmationMail</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Event</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertDispatched</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">OrderCreated</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Queue</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">assertPushed</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">ProcessInvoice</span><span style="color: #D4D4D4">::</span><span style="color: #569CD6">class</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;payments&#39;</span><span style="color: #D4D4D4">, &#091;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;completed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#093;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Cache</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">has</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;user:{</span><span style="color: #9CDCFE">$user</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">id</span><span style="color: #CE9178">}:orders&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #4EC9B0">Log</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">shouldHaveReceived</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;info&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Hardcoded values have a habit of surviving far longer than they should.</p>



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

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



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



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



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



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



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



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



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



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



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



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



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

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



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



<p class="has-d-4-d-4-d-4-color has-text-color wp-block-paragraph">Instead, create separate test cases.</p>



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

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



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



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



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



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



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



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



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



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



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

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



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

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

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



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



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



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



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



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



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



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



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

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

    $response->assertCreated();

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

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

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

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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


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



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

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



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



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



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



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



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



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



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



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



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



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



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



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

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

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

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



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



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



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



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



<p class="wp-block-paragraph">Consider a more ambitious implementation of <code>ship()</code>:</p>



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

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

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

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

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

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

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

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



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



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



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



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



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



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



<p class="wp-block-paragraph">Whether your team calls it an action, command, handler, or service matters less than giving it a clear responsibility.</p>



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

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

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

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

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

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

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

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

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

        if (! $this->inventory->isAvailableFor($order)) {
            throw new CannotShipOrder('One or more items are unavailable.');
        }
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">namespace</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">App\Actions</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Models\</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Notifications\</span><span style="color: #4EC9B0">OrderShippedNotification</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Services\</span><span style="color: #4EC9B0">InventoryService</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> App\Services\</span><span style="color: #4EC9B0">ShippingGateway</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Support\Facades\</span><span style="color: #4EC9B0">DB</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">final</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShipOrder</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">InventoryService</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$inventory</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ShippingGateway</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$shipping</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">ensureOrderCanBeShipped</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$shipment</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">shipping</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #4EC9B0">DB</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">transaction</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> () </span><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$shipment</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span><span style="color: #D4D4D4"> {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">update</span><span style="color: #D4D4D4">(&#091;</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;status&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;shipped&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;shipment_reference&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$shipment</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">reference</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #CE9178">&#39;shipped_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">            &#093;);</span></span>
<span class="line"><span style="color: #D4D4D4">        });</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">customer</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">notify</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderShippedNotification</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">ensureOrderCanBeShipped</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (! </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isPaid</span><span style="color: #D4D4D4">()) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CannotShipOrder</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;The order has not been paid.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (! </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">hasShippingAddress</span><span style="color: #D4D4D4">()) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CannotShipOrder</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;The shipping address is missing.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (! </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">inventory</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isAvailableFor</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">)) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CannotShipOrder</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;One or more items are unavailable.&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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


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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<li>one week</li>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<li>updating failing tests</li>



<li>adapting to refactored services</li>



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/07/26/merge-conflicts-process-problem/">Merge Conflicts Are a Process Problem, Not a Git Problem</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/07/26/merge-conflicts-process-problem/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Testing Laravel Events and Listeners: Ensuring Reliable Asynchronous Workflows</title>
		<link>https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/</link>
					<comments>https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 13 Jul 2026 16:37:00 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3314</guid>

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



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



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



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



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



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



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

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

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



<p class="wp-block-paragraph">This test tells us one thing: <em>Did we trigger the event?</em> It tells us absolutely nothing about whether the listeners actually work or if they communicate correctly with each other. If you rely solely on faking, your test suite becomes a &#8220;smoke screen&#8221; that passes even when your underlying infrastructure is broken.</p>



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



<p class="wp-block-paragraph">The best way to ensure reliability is to treat your Listeners like any other service class. A listener should have one job. If it has complex logic, extract it into a dedicated Action or Service class and test <em>that</em>.</p>



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



<p class="wp-block-paragraph">By keeping the listener thin, your unit test for <code>SendOrderConfirmation</code> becomes trivial. You can mock the <code>Mailer</code> service and simply verify the <code>handle()</code> method is called correctly. This is fast, deterministic, and catches bugs in the communication layer without overhead.</p>



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



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



<p class="wp-block-paragraph">Crucially, <strong>do not fake the event here.</strong> Instead, allow the event to dispatch and verify the side effects.</p>



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

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

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



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



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



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



<p class="wp-block-paragraph">To test queued listeners, use the <code>Bus</code> and <code>Queue</code> facades in tandem:</p>



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

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

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



<p class="wp-block-paragraph"><strong>Pro Tip:</strong> If you are testing a complex flow, don&#8217;t forget to test the failure state. Use <code>Queue::assertPushed</code> to verify that the retry logic or failure handling (like <code>failed()</code> methods) is configured for your mission-critical jobs.</p>



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/">Testing Laravel Events and Listeners: Ensuring Reliable Asynchronous Workflows</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/07/13/testing-laravel-events-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
