<?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>CodeCraft Diary</title>
	<atom:link href="https://codecraftdiary.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://codecraftdiary.com/</link>
	<description></description>
	<lastBuildDate>Fri, 29 May 2026 09:02:21 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://codecraftdiary.com/wp-content/uploads/2025/10/cropped-IMG_3463-32x32.png</url>
	<title>CodeCraft Diary</title>
	<link>https://codecraftdiary.com/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Flaky Tests in Laravel: Why Your CI Randomly Fails</title>
		<link>https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/</link>
					<comments>https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 15:00:00 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3285</guid>

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">Consider this example:</p>



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

    sleep(1);

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



<p class="wp-block-paragraph">This test may pass most of the time.</p>



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



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



<li>server load</li>



<li>execution timing</li>



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



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



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



<p class="wp-block-paragraph">Use fixed time.</p>



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

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

$this->assertFalse($subscription->isExpired());
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #4EC9B0">Carbon</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">setTestNow</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;2026-05-28 10:00:00&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #9CDCFE">$subscription</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Subscription</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">factory</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;expires_at&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #DCDCAA">now</span><span style="color: #D4D4D4">()-&gt;</span><span style="color: #DCDCAA">addDay</span><span style="color: #D4D4D4">(),</span></span>
<span class="line"><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertFalse</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$subscription</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">isExpired</span><span style="color: #D4D4D4">());</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">And always clean up afterwards:</p>



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



<p class="wp-block-paragraph">Without cleanup, fake time can leak into other tests and create even more randomness.</p>



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



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



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



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



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



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

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



<p class="wp-block-paragraph">At first, this looks harmless. However, once another test inserts posts into the database, the count may suddenly become 2, 5, or even 12.</p>



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



<p class="wp-block-paragraph">In Laravel, this usually means:</p>



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



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



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



<p class="wp-block-paragraph">depending on your architecture.</p>



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">This test looks innocent:</p>



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

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



<p class="wp-block-paragraph">But if the factory generates random roles, this test becomes unstable immediately.</p>



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



<p class="wp-block-paragraph">Instead, explicitly define required state:</p>



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



<p class="wp-block-paragraph">Deterministic data creates deterministic tests.</p>



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



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



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



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



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



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

dispatch(new SendInvoiceJob($invoice));

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



<p class="wp-block-paragraph">This can fail because the queued job never actually runs.</p>



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



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



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



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

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



<p class="wp-block-paragraph">The assertion may execute before the worker finishes.</p>



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



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



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



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



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



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



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

dispatch(new SyncProductsJob());

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



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



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



<p class="wp-block-paragraph">during the test environment.</p>



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



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



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



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



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



<li>shared files</li>



<li>cached config</li>



<li>temporary directories</li>



<li>static variables</li>



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



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



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



<p class="wp-block-paragraph">If multiple tests write the same file simultaneously, random failures appear.</p>



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



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



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



<p class="wp-block-paragraph">or unique filenames:</p>



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



<p class="wp-block-paragraph">Parallel testing does not create flaky tests.</p>



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">Laravel provides excellent HTTP faking:</p>



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



<p class="wp-block-paragraph">Now your tests become:</p>



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



<li>deterministic</li>



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



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



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



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



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



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



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



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



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



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



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



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



<li>repeatedly</li>



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



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



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



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



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



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



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



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



<li>reruns become normal</li>



<li>real bugs get missed</li>



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



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



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



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



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



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



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



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



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



<li>uncontrolled time</li>



<li>async behavior</li>



<li>non-isolated infrastructure</li>



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



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



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



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



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



<li>in CI</li>



<li>on every machine</li>



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



<li>Basic validation</li>



<li>API serialization</li>



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



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



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

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



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



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



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



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



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



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

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

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

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

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



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



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



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



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



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



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



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



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



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



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



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



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

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

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



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



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

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

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

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



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



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

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

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



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



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



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



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



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



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



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



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



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



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

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



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



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



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

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

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

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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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



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



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



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



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



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



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



<p class="wp-block-paragraph">Consider this classic trap:</p>



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

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

        return $price;
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">&lt;?php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderDiscountCalculator</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">calculate</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$price</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">float</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4"> &gt;= </span><span style="color: #B5CEA8">10</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$price</span><span style="color: #D4D4D4"> * </span><span style="color: #B5CEA8">0.9</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$price</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">And a test that covers it 100%:</p>



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

use PHPUnit\Framework\TestCase;

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

        // Both branches hit — 100% coverage!
        $calculator->calculate(100.0, 15);
        $calculator->calculate(100.0, 5);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">&lt;?php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> PHPUnit\Framework\</span><span style="color: #4EC9B0">TestCase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderDiscountCalculatorTest</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TestCase</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">testCalculate</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$calculator</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderDiscountCalculator</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Both branches hit — 100% coverage!</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$calculator</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">calculate</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">100.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">15</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$calculator</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">calculate</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">100.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">This test covers 100% of the code. It also asserts absolutely nothing. If someone changes <code>0.9</code> to <code>0.5</code>, your test suite stays green while your customers get 50% off everything. That&#8217;s a very expensive bug.</p>



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



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph"><a href="https://infection.github.io/">Infection</a> is the de facto mutation testing framework for PHP. It integrates cleanly with PHPUnit and runs as a Composer dev dependency.</p>



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



<p class="wp-block-paragraph">Run it for the first time with:</p>



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



<p class="wp-block-paragraph">Infection will run your existing test suite, then start generating and testing mutants. On a modern project with <code>--threads=4</code>, it&#8217;s fast enough to include in a CI pipeline.</p>



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



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



<p class="wp-block-paragraph">Let me walk you through a scenario I actually encountered on a SaaS project — a pricing engine with tiered discounts.</p>



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

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

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

        return round($unitPrice * $quantity, 2);
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">&lt;?php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TieredPricingService</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">const</span><span style="color: #D4D4D4"> TIERS = &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #B5CEA8">100</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">0.70</span><span style="color: #D4D4D4">, </span><span style="color: #6A9955">// 30% discount for 100+</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #B5CEA8">50</span><span style="color: #D4D4D4">  =&gt; </span><span style="color: #B5CEA8">0.80</span><span style="color: #D4D4D4">, </span><span style="color: #6A9955">// 20% discount for 50+</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #B5CEA8">10</span><span style="color: #D4D4D4">  =&gt; </span><span style="color: #B5CEA8">0.90</span><span style="color: #D4D4D4">, </span><span style="color: #6A9955">// 10% discount for 10+</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">getPrice</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">float</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">::TIERS as </span><span style="color: #9CDCFE">$minQuantity</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$multiplier</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4"> &gt;= </span><span style="color: #9CDCFE">$minQuantity</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">                </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">round</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$multiplier</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">2</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">            }</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">round</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">2</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">My original tests covered all branches. PHPUnit reported 100% coverage. But when I ran Infection, it flagged a surviving mutant — it changed <code>&gt;=</code> to <code>&gt;</code> in the tier check, and my test for exactly 10 units didn&#8217;t catch it because I only tested with 11. The boundary condition was untested.</p>



<p class="wp-block-paragraph">Here&#8217;s what the corrected test looked like after Infection exposed the gap:</p>



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

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

class TieredPricingServiceTest extends TestCase
{
    private TieredPricingService $service;

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

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

    public static function pricingProvider(): array
    {
        return [
            'below first tier'         => &#91;10.0, 5,   50.00&#93;,
            'exactly at 10 tier'       => &#91;10.0, 10,  90.00&#93;,  // boundary — was missing!
            'above 10 tier'            => &#91;10.0, 11,  99.00&#93;,
            'exactly at 50 tier'       => &#91;10.0, 50,  400.00&#93;, // boundary
            'exactly at 100 tier'      => &#91;10.0, 100, 700.00&#93;, // boundary
            'above highest tier'       => &#91;10.0, 200, 1400.00&#93;,
        ];
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">&lt;?php</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> PHPUnit\Framework\</span><span style="color: #4EC9B0">TestCase</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> PHPUnit\Framework\Attributes\</span><span style="color: #4EC9B0">DataProvider</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TieredPricingServiceTest</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TestCase</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">private</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TieredPricingService</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">protected</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">setUp</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">service</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">TieredPricingService</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    #&#91;DataProvider(</span><span style="color: #CE9178">&#39;pricingProvider&#39;</span><span style="color: #D4D4D4">)&#93;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">testGetPrice</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$expected</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">void</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertSame</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$expected</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">getPrice</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$unitPrice</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$quantity</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">pricingProvider</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">array</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> [</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;below first tier&#39;</span><span style="color: #D4D4D4">         =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">5</span><span style="color: #D4D4D4">,   </span><span style="color: #B5CEA8">50.00</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;exactly at 10 tier&#39;</span><span style="color: #D4D4D4">       =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">10</span><span style="color: #D4D4D4">,  </span><span style="color: #B5CEA8">90.00</span><span style="color: #D4D4D4">&#93;,  </span><span style="color: #6A9955">// boundary — was missing!</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;above 10 tier&#39;</span><span style="color: #D4D4D4">            =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">11</span><span style="color: #D4D4D4">,  </span><span style="color: #B5CEA8">99.00</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;exactly at 50 tier&#39;</span><span style="color: #D4D4D4">       =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">50</span><span style="color: #D4D4D4">,  </span><span style="color: #B5CEA8">400.00</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #6A9955">// boundary</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;exactly at 100 tier&#39;</span><span style="color: #D4D4D4">      =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">100</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">700.00</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #6A9955">// boundary</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;above highest tier&#39;</span><span style="color: #D4D4D4">       =&gt; &#91;</span><span style="color: #B5CEA8">10.0</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">200</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">1400.00</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">        ];</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">After adding boundary assertions, Infection&#8217;s MSI jumped from 61% to 94%. That&#8217;s the difference between a test suite that gives you false confidence and one that actually has your back.</p>



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



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



<p class="wp-block-paragraph">Infection is configured via <code>infection.json5</code> in your project root. Here&#8217;s a production-ready config I use:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(2 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>{
    "$schema": "vendor/infection/infection/resources/schema.json",
    "source": {
        "directories": &#91;"src"&#93;,
        "excludes": &#91;"src/Infrastructure/Migrations"&#93;
    },
    "mutators": {
        "@default": true
    },
    "testFramework": "phpunit",
    "testFrameworkOptions": "--testsuite=unit",
    "minMsi": 85,
    "minCoveredMsi": 90,
    "threads": 4,
    "logs": {
        "text": "var/log/infection.log",
        "html": "var/log/infection.html",
        "summary": "var/log/infection-summary.log"
    }
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;</span><span style="color: #9CDCFE">$schema</span><span style="color: #CE9178">&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;vendor/infection/infection/resources/schema.json&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;source&quot;</span><span style="color: #D4D4D4">: {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;directories&quot;</span><span style="color: #D4D4D4">: &#91;</span><span style="color: #CE9178">&quot;src&quot;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;excludes&quot;</span><span style="color: #D4D4D4">: &#91;</span><span style="color: #CE9178">&quot;src/Infrastructure/Migrations&quot;</span><span style="color: #D4D4D4">&#93;</span></span>
<span class="line"><span style="color: #D4D4D4">    },</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;mutators&quot;</span><span style="color: #D4D4D4">: {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;@default&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #569CD6">true</span></span>
<span class="line"><span style="color: #D4D4D4">    },</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;testFramework&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;phpunit&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;testFrameworkOptions&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;--testsuite=unit&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;minMsi&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">85</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;minCoveredMsi&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">90</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;threads&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #B5CEA8">4</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&quot;logs&quot;</span><span style="color: #D4D4D4">: {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;text&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;var/log/infection.log&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;html&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;var/log/infection.html&quot;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&quot;summary&quot;</span><span style="color: #D4D4D4">: </span><span style="color: #CE9178">&quot;var/log/infection-summary.log&quot;</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">The <code>minMsi</code> and <code>minCoveredMsi</code> thresholds are important — they let your CI pipeline fail if mutation score drops below acceptable levels, the same way PHPUnit can fail below a coverage threshold.</p>



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



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



<p class="wp-block-paragraph">Here&#8217;s a GitHub Actions job I&#8217;ve been running since mid-2025:</p>



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

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

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

    - name: Run Infection
      run: ./vendor/bin/infection --threads=4 --min-msi=85 --min-covered-msi=90
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">mutation-testing:</span></span>
<span class="line"><span style="color: #D4D4D4">  runs-</span><span style="color: #569CD6">on</span><span style="color: #D4D4D4">: ubuntu-latest</span></span>
<span class="line"><span style="color: #D4D4D4">  needs: tests</span></span>
<span class="line"><span style="color: #D4D4D4">  steps:</span></span>
<span class="line"><span style="color: #D4D4D4">    - uses: actions/checkout@v4</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    - name: Setup PHP</span></span>
<span class="line"><span style="color: #D4D4D4">      uses: shivammathur/setup-php@v2</span></span>
<span class="line"><span style="color: #D4D4D4">      with:</span></span>
<span class="line"><span style="color: #D4D4D4">        php-version: </span><span style="color: #CE9178">&#39;8.5&#39;</span></span>
<span class="line"><span style="color: #D4D4D4">        coverage: xdebug</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    - name: Install dependencies</span></span>
<span class="line"><span style="color: #D4D4D4">      run: composer install --</span><span style="color: #569CD6">no</span><span style="color: #D4D4D4">-interaction</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    - name: Run Infection</span></span>
<span class="line"><span style="color: #D4D4D4">      run: </span><span style="color: #D4D4D4">.</span><span style="color: #D4D4D4">/vendor/bin/infection --threads=</span><span style="color: #B5CEA8">4</span><span style="color: #D4D4D4"> --min-msi=</span><span style="color: #B5CEA8">85</span><span style="color: #D4D4D4"> --min-covered-msi=</span><span style="color: #B5CEA8">90</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">One important note: Infection requires a coverage driver (Xdebug or PCOV) to know which mutants are relevant to which tests. PCOV is faster for large codebases; Xdebug gives more detail. I use Xdebug locally and PCOV in CI.</p>



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



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



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



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



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



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



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



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



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



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



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



<p class="wp-block-paragraph">The <code>--git-diff-filter</code> flag is a game-changer for larger repos — it only mutates code touched in the current diff, so mutation testing stays practical even on monorepos.</p>



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



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



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



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



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



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



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



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



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



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



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



<li><a href="https://phpunit.de/">PHPUnit documentation</a></li>
</ul>
<p>The post <a href="https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/">How Mutation Testing Exposes the Truth (PHP 2026 Edition)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Mastering Value Objects in PHP 8.5+ (2026 Edition)</title>
		<link>https://codecraftdiary.com/2026/05/02/mastering-value-objects-in-php/</link>
					<comments>https://codecraftdiary.com/2026/05/02/mastering-value-objects-in-php/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 02 May 2026 17:17:22 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<category><![CDATA[cleancode]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[refactoring]]></category>
		<category><![CDATA[software-design]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3247</guid>

					<description><![CDATA[<p>As developers, we often have a problematic relationship with primitives. We use a string for an email, a float for a price, and an int for a status. This is what we call Primitive Obsession—and it’s one of the common reasons why PHP codebases gradually become hard to maintain. If you’ve been following my series [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/05/02/mastering-value-objects-in-php/">Mastering Value Objects in PHP 8.5+ (2026 Edition)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">As developers, we often have a problematic relationship with primitives. We use a string for an email, a float for a price, and an int for a status. This is what we call <em>Primitive Obsession</em>—and it’s one of the common reasons why PHP codebases gradually become hard to maintain.</p>



<p class="wp-block-paragraph">If you’ve been following my series on Refactoring &amp; Patterns, you know I’m a fan of the <em>Introduce Parameter Object</em> pattern. But today, I want to go deeper and talk about one of the smallest, yet most powerful building blocks of clean architecture: <strong>Value Objects</strong>.</p>



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



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-650e3ccc515ec1cbd84ffb3bce80be0e">The “Price” of Primitive Obsession</h2>



<p class="wp-block-paragraph">Imagine you’re working on an e-commerce platform. You have a Product and a Discount.</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 applyDiscount(float $price, float $discountPercentage):float
{
    if ($discountPercentage &lt; 0 || $discountPercentage > 100) {
        throw new InvalidArgumentException("Invalid discount");
    }

    return $price - ($price * ($discountPercentage / 100));
}</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">applyDiscount</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$price</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">float</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$discountPercentage</span><span style="color: #D4D4D4">):</span><span style="color: #569CD6">float</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$discountPercentage</span><span style="color: #D4D4D4"> &lt; </span><span style="color: #B5CEA8">0</span><span style="color: #D4D4D4"> || </span><span style="color: #9CDCFE">$discountPercentage</span><span style="color: #D4D4D4"> &gt; </span><span style="color: #B5CEA8">100</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">InvalidArgumentException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;Invalid discount&quot;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$price</span><span style="color: #D4D4D4"> - (</span><span style="color: #9CDCFE">$price</span><span style="color: #D4D4D4"> * (</span><span style="color: #9CDCFE">$discountPercentage</span><span style="color: #D4D4D4"> / </span><span style="color: #B5CEA8">100</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">At first glance, this looks fine. But in a real-world application, that <code>$price</code> is floating around (pun intended) everywhere.</p>



<ul class="wp-block-list">
<li>Is it USD or EUR?</li>



<li>Does it include VAT?</li>



<li>What about rounding?</li>
</ul>



<p class="wp-block-paragraph">And more importantly: what happens if you accidentally pass <code>$discountPercentage</code> as <code>$price</code>?</p>



<p class="wp-block-paragraph">PHP won’t complain. Both are floats. You just sold a MacBook for $15.</p>



<p class="wp-block-paragraph">On top of that, floats introduce precision issues, which makes them a poor choice for financial calculations in the first place.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-5b5787799f6c9164930d21df78738b88">What Exactly is a Value Object?</h2>



<p class="wp-block-paragraph">A <strong>Value Object (VO)</strong> is an object that is defined by its value rather than its identity.</p>



<p class="wp-block-paragraph">Two Value Objects with the same data are considered equal—even if they are different instances.</p>



<p class="wp-block-paragraph">In modern PHP (8.2+), a well-designed Value Object has three key characteristics:</p>



<ul class="wp-block-list">
<li><strong>Immutability</strong> – once created, it cannot change</li>



<li><strong>Validation</strong> – it cannot exist in an invalid state</li>



<li><strong>Self-documentation</strong> – the type clearly expresses intent</li>
</ul>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-addca7305c21ef3aa8b2a8ef183b7762">A Better Approach: Explicit Domain Types</h2>



<p class="wp-block-paragraph">Let’s refactor the previous 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>final readonly class Price
{
    public function __construct(
        public int $amount, // in cents
        public Currency $currency
    ) {
        if ($this->amount &lt; 0) {
            throw new InvalidPriceException("Price cannot be negative.");
        }
    }

    public function add(Price $other): Price
    {
        if ($this->currency !== $other->currency) {
            throw new CurrencyMismatchException();
        }

        return new Price($this->amount + $other->amount, $this->currency);
    }

    public function equals(Price $other): bool
    {
        return $this->amount === $other->amount
            &amp;&amp; $this->currency === $other->currency;
    }
}</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">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Price</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">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$amount</span><span style="color: #D4D4D4">, </span><span style="color: #6A9955">// in cents</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Currency</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$currency</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">amount</span><span style="color: #D4D4D4"> &lt; </span><span style="color: #B5CEA8">0</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">InvalidPriceException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;Price cannot be negative.&quot;</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">add</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Price</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$other</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">Price</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">currency</span><span style="color: #D4D4D4"> !== </span><span style="color: #9CDCFE">$other</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">currency</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">CurrencyMismatchException</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: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Price</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">amount</span><span style="color: #D4D4D4"> + </span><span style="color: #9CDCFE">$other</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">amount</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">currency</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">equals</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Price</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$other</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">amount</span><span style="color: #D4D4D4"> === </span><span style="color: #9CDCFE">$other</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">amount</span></span>
<span class="line"><span style="color: #D4D4D4">            &amp;&amp; </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">currency</span><span style="color: #D4D4D4"> === </span><span style="color: #9CDCFE">$other</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">currency</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">A few important things are happening here:</p>



<ul class="wp-block-list">
<li><strong>Encapsulation</strong> – price logic lives inside the <code>Price</code> class</li>



<li><strong>Type safety</strong> – you cannot mix currencies accidentally</li>



<li><strong>Immutability</strong> – every operation returns a new instance</li>



<li><strong>Precision</strong> – using integers avoids float rounding issues</li>
</ul>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-b54bc2c4d6ef37509aa96c83fbe57232">Why This Matters (Especially Today)</h2>



<p class="wp-block-paragraph">With AI-assisted development becoming standard, types matter more than ever.</p>



<p class="wp-block-paragraph">When you use primitives, tools like GitHub Copilot or ChatGPT have to <em>guess</em> intent.</p>



<p class="wp-block-paragraph">When you use a <code>Price</code> or <code>EmailAddress</code> object, both humans and AI can:</p>



<ul class="wp-block-list">
<li>understand constraints immediately</li>



<li>discover available behavior via methods</li>



<li>avoid invalid states by design</li>
</ul>



<p class="wp-block-paragraph">You’re not just writing code—you’re defining a <strong>clear contract</strong>.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-9da57dff588b9f33a571a80467342b65">Real-World Refactoring: Email</h2>



<p class="wp-block-paragraph">How often have you written this?</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(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>if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {&lt;br>    throw new Exception("Invalid email");&lt;br>}</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: #C586C0">if</span><span style="color: #D4D4D4"> (!</span><span style="color: #DCDCAA">filter_var</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$email</span><span style="color: #D4D4D4">, FILTER_VALIDATE_EMAIL)) {&lt;</span><span style="color: #569CD6">br</span><span style="color: #D4D4D4">&gt;    </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">Exception</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;Invalid email&quot;</span><span style="color: #D4D4D4">);&lt;</span><span style="color: #569CD6">br</span><span style="color: #D4D4D4">&gt;}</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">If it appears in multiple places, that’s duplication—and a maintenance risk.</p>



<p class="wp-block-paragraph">Let’s move that logic into a Value 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>final readonly class EmailAddress
{
    private string $value;

    public function __construct(string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidEmailException($value);
        }

        $this->value = strtolower(trim($value));
    }

    public function getDomain(): string
    {
        return substr(strrchr($this->value, "@"), 1);
    }

    public function __toString(): string
    {
        return $this->value;
    }
}</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">readonly</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">EmailAddress</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">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$value</span><span style="color: #D4D4D4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">__construct</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$value</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">if</span><span style="color: #D4D4D4"> (!</span><span style="color: #DCDCAA">filter_var</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$value</span><span style="color: #D4D4D4">, FILTER_VALIDATE_EMAIL)) {</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">InvalidEmailException</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$value</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: #9CDCFE">value</span><span style="color: #D4D4D4"> = </span><span style="color: #DCDCAA">strtolower</span><span style="color: #D4D4D4">(</span><span style="color: #DCDCAA">trim</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$value</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">getDomain</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">substr</span><span style="color: #D4D4D4">(</span><span style="color: #DCDCAA">strrchr</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">value</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&quot;@&quot;</span><span style="color: #D4D4D4">), </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"></span>
<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">__toString</span><span style="color: #D4D4D4">(): </span><span style="color: #569CD6">string</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">value</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">Now your service layer becomes much cleaner:</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>// BEFORE
public function registerUser(string $email, string $password) { ... }

// AFTER
public function registerUser(EmailAddress $email, Password $password) { ... }</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">// BEFORE</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">registerUser</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$email</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$password</span><span style="color: #D4D4D4">) { ... }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6A9955">// AFTER</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">registerUser</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">EmailAddress</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$email</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">Password</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$password</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">The moment execution reaches <code>registerUser</code>, you already know the email is valid.</p>



<p class="wp-block-paragraph">Validation is handled at the boundary of your system—not scattered across your codebase.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-e4cf6b7aad5b85602632f056bcb702e6">Logic-Heavy Value Objects</h2>



<p class="wp-block-paragraph">A common mistake is treating Value Objects as simple data containers.</p>



<p class="wp-block-paragraph">In practice, they should encapsulate <strong>behavior related to that data</strong>.</p>



<p class="wp-block-paragraph">Instead of passing:</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>string $startDate, string $endDate</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">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$startDate</span><span style="color: #D4D4D4">, </span><span style="color: #569CD6">string</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$endDate</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">You can 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>OrderDateRange</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">OrderDateRange</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">With methods like:</p>



<ul class="wp-block-list">
<li><code>overlapsWith()</code></li>



<li><code>isWithinLastMonth()</code></li>



<li><code>getDurationInDays()</code></li>
</ul>



<p class="wp-block-paragraph">This reduces cognitive load in your services and keeps domain logic where it belongs.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-76ac94d63131b6472703b95981bcedeb">When NOT to Use Value Objects</h2>



<p class="wp-block-paragraph">Not everything needs to be a Value Object.</p>



<p class="wp-block-paragraph">Ask yourself:</p>



<ul class="wp-block-list">
<li>Does this data have validation rules?</li>



<li>Is it reused in multiple places?</li>



<li>Does it represent a domain concept (SKU, IBAN, Email, Price)?</li>
</ul>



<p class="wp-block-paragraph">If the answer is <em>yes</em>, a Value Object is likely justified.</p>



<p class="wp-block-paragraph">If you’re building a quick prototype, primitives are fine. Just be aware of the trade-offs.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-36a13f661653c96be4d1a1887192e82d">Performance Considerations</h2>



<p class="wp-block-paragraph">A common concern used to be performance—creating many small objects instead of using primitives.</p>



<p class="wp-block-paragraph">In modern PHP, object instantiation is highly optimized. The overhead is negligible compared to the cost of bugs caused by invalid states.</p>



<p class="wp-block-paragraph">More importantly:</p>



<ul class="wp-block-list">
<li>immutable objects are predictable</li>



<li>they eliminate side effects</li>



<li>they are naturally safe in concurrent or async contexts</li>
</ul>



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



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



<p class="wp-block-paragraph">Refactoring toward Value Objects is one of the most effective ways to improve code quality.</p>



<p class="wp-block-paragraph">It forces you to think in terms of <strong>domain concepts</strong>, not just data types.</p>



<h3 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-0b777e77e9c004b85c9f8f03c6df2206">Practical steps:</h3>



<ul class="wp-block-list">
<li>Look at a complex service class</li>



<li>Find a variable validated in multiple places</li>



<li>Extract it into a readonly Value Object</li>



<li>Move related logic into that object</li>
</ul>



<p class="wp-block-paragraph">You’ll end up with code that is easier to read, safer to modify, and harder to break.</p>



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



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/05/02/mastering-value-objects-in-php/">Mastering Value Objects in PHP 8.5+ (2026 Edition)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/05/02/mastering-value-objects-in-php/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Trunk-Based Development: Your Pull Requests Are Still Too Big</title>
		<link>https://codecraftdiary.com/2026/04/29/trunk-based-development-your-pull-requests-are-still-too-big/</link>
					<comments>https://codecraftdiary.com/2026/04/29/trunk-based-development-your-pull-requests-are-still-too-big/#comments</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Wed, 29 Apr 2026 18:14:03 +0000</pubDate>
				<category><![CDATA[Development Workflow & Best Practices]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[workflow]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3240</guid>

					<description><![CDATA[<p>Most teams don’t realize this, but their biggest bottleneck isn’t architecture, tech stack, or even legacy code. It’s pull requests. If you read about trunk-based development, you’ll see the same advice repeated everywhere: small changes, frequent merges, fast feedback. Sounds simple. Almost obvious. And yet — in reality — most teams are nowhere near that. [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/04/29/trunk-based-development-your-pull-requests-are-still-too-big/">Trunk-Based Development: Your Pull Requests Are Still Too Big</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Most teams don’t realize this, but their biggest bottleneck isn’t architecture, tech stack, or even legacy code.</p>



<p class="wp-block-paragraph">It’s pull requests.</p>



<p class="wp-block-paragraph">If you read about trunk-based development, you’ll see the same advice repeated everywhere: small changes, frequent merges, fast feedback. Sounds simple. Almost obvious.</p>



<p class="wp-block-paragraph">And yet — in reality — most teams are nowhere near that.</p>



<p class="wp-block-paragraph">I’ve personally seen pull requests sitting open for months. Not days. Not weeks. Months.</p>



<p class="wp-block-paragraph">At one point, we had a pull request in our team that was open for more than <strong>six months</strong>.</p>



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



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



<p class="wp-block-paragraph">At that point, it’s no longer a pull request. It’s a parallel universe.</p>



<p class="wp-block-paragraph"><strong>Below this link you can read first article about Trunk-Based Development</strong> <a href="https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/</a></p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-dd76ccf54ed6773068e3f7dbfd9f773c">The Hidden Cost of Large Pull Requests</h2>



<p class="wp-block-paragraph">Let’s be precise about what’s happening here.</p>



<p class="wp-block-paragraph">A large pull request is not just “a bit harder to review”. It fundamentally breaks your delivery system.</p>



<p class="wp-block-paragraph">Here’s what actually happens:</p>



<h3 class="wp-block-heading">1. Review becomes a cognitive nightmare</h3>



<p class="wp-block-paragraph">When a PR has:</p>



<ul class="wp-block-list">
<li>1,000+ lines</li>



<li>multiple concerns (API, DB, UI, validation)</li>



<li>partial refactors</li>
</ul>



<p class="wp-block-paragraph">No one can realistically review it properly.</p>



<p class="wp-block-paragraph">So what happens?</p>



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



<li>they miss edge cases</li>



<li>they delay the review (“I’ll look at it later”)</li>
</ul>



<p class="wp-block-paragraph">And “later” turns into never.</p>



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



<h3 class="wp-block-heading">2. Feedback loop collapses</h3>



<p class="wp-block-paragraph">Fast feedback is the core of modern development.</p>



<p class="wp-block-paragraph">But with large PRs:</p>



<ul class="wp-block-list">
<li>feedback comes late</li>



<li>feedback is vague</li>



<li>feedback is expensive to apply</li>
</ul>



<p class="wp-block-paragraph">Instead of:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“Fix this small thing”</p>
</blockquote>



<p class="wp-block-paragraph">You get:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“This entire approach might be wrong”</p>
</blockquote>



<p class="wp-block-paragraph">Now the author has to:</p>



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



<li>rework large chunks of code</li>



<li>re-request review</li>
</ul>



<p class="wp-block-paragraph">Cycle time explodes.</p>



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



<h3 class="wp-block-heading">3. Merge becomes risky</h3>



<p class="wp-block-paragraph">The bigger the PR, the higher the risk:</p>



<ul class="wp-block-list">
<li>conflicts with main branch</li>



<li>outdated assumptions</li>



<li>broken integrations</li>
</ul>



<p class="wp-block-paragraph">So teams hesitate.</p>



<p class="wp-block-paragraph">And hesitation kills flow.</p>



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



<h3 class="wp-block-heading">4. Work gets batched (and everything slows down)</h3>



<p class="wp-block-paragraph">This is the core anti-pattern.</p>



<p class="wp-block-paragraph">Instead of shipping continuously, developers start batching work:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“I’ll just add one more thing before I open the PR”</p>
</blockquote>



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



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“Actually, I’ll include this refactor too”</p>
</blockquote>



<p class="wp-block-paragraph">And suddenly:</p>



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



<li>review slows</li>



<li>merge is delayed</li>
</ul>



<p class="wp-block-paragraph">It’s a vicious cycle.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-b78d1bc88a399a3e2e32306326578a1f">The Real Reason Your PRs Are Too Big</h2>



<p class="wp-block-paragraph">Let’s be honest — this is not a tooling issue.</p>



<p class="wp-block-paragraph">It’s behavioral.</p>



<p class="wp-block-paragraph">Here are the real causes:</p>



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



<h3 class="wp-block-heading">1. “I want it to be complete”</h3>



<p class="wp-block-paragraph">This is the most common mindset:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“I’ll open the PR when the feature is done.”</p>
</blockquote>



<p class="wp-block-paragraph">Sounds reasonable. It’s also completely wrong.</p>



<p class="wp-block-paragraph">Because “done” often means:</p>



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



<li>edge cases</li>



<li>polish</li>
</ul>



<p class="wp-block-paragraph">Which leads to massive PRs.</p>



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



<h3 class="wp-block-heading">2. Fear of breaking things</h3>



<p class="wp-block-paragraph">Without proper safety mechanisms (tests, feature flags), developers avoid small merges.</p>



<p class="wp-block-paragraph">So they wait.</p>



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



<p class="wp-block-paragraph">And accumulate changes.</p>



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



<h3 class="wp-block-heading">3. Poor slicing of work</h3>



<p class="wp-block-paragraph">Most developers split work like this:</p>



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



<li>frontend PR</li>



<li>DB migration PR</li>
</ul>



<p class="wp-block-paragraph">Instead of:</p>



<ul class="wp-block-list">
<li>vertical slices that deliver value end-to-end</li>
</ul>



<p class="wp-block-paragraph">This creates artificial dependencies and forces larger PRs.</p>



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



<h3 class="wp-block-heading">4. Review culture is asynchronous and slow</h3>



<p class="wp-block-paragraph">If reviews take:</p>



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



<li>or days</li>
</ul>



<p class="wp-block-paragraph">Developers adapt by batching work.</p>



<p class="wp-block-paragraph">Why open a small PR if it will just sit there?</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-de879e4922314df9be75c2a68ee87d3c">The 6-Month Pull Request Problem</h2>



<p class="wp-block-paragraph">Let’s go back to that real example.</p>



<p class="wp-block-paragraph">A PR open for six months.</p>



<p class="wp-block-paragraph">What does that actually mean?</p>



<ul class="wp-block-list">
<li>The branch is completely outdated</li>



<li>The context is lost</li>



<li>The author doesn’t remember all decisions</li>



<li>The reviewers don’t understand it anymore</li>
</ul>



<p class="wp-block-paragraph">At that point, you have only two realistic options:</p>



<ol class="wp-block-list">
<li>Merge it blindly (high risk)</li>



<li>Close it and start over (lost work)</li>
</ol>



<p class="wp-block-paragraph">Both are bad.</p>



<p class="wp-block-paragraph">But the real failure happened much earlier — when the PR was allowed to grow beyond control.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-e834550bf13eff457e1724662e07e49c">How to Fix This (Practically)</h2>



<p class="wp-block-paragraph">This is not about theory. These are concrete practices that work.</p>



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



<h3 class="wp-block-heading">1. Enforce a hard PR size limit</h3>



<p class="wp-block-paragraph">Set a rule:</p>



<ul class="wp-block-list">
<li><strong>Max ~300–400 lines per PR</strong></li>
</ul>



<p class="wp-block-paragraph">Not as a suggestion.</p>



<p class="wp-block-paragraph">As a constraint.</p>



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



<p class="wp-block-paragraph">Because constraints force better behavior:</p>



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



<li>fewer concerns per change</li>



<li>faster reviews</li>
</ul>



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



<h3 class="wp-block-heading">2. Merge incomplete work (safely)</h3>



<p class="wp-block-paragraph">This is the shift most teams struggle with.</p>



<p class="wp-block-paragraph">You don’t need “finished features” to merge.</p>



<p class="wp-block-paragraph">You need:</p>



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



<li>controlled exposure</li>
</ul>



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



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



<li>toggles</li>



<li>hidden UI states</li>
</ul>



<p class="wp-block-paragraph">This allows you to:</p>



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



<li>iterate safely</li>
</ul>



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



<h3 class="wp-block-heading">3. Slice vertically, not horizontally</h3>



<p class="wp-block-paragraph">Instead of:</p>



<ul class="wp-block-list">
<li>PR 1: database</li>



<li>PR 2: API</li>



<li>PR 3: UI</li>
</ul>



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



<ul class="wp-block-list">
<li>PR 1: minimal end-to-end functionality</li>



<li>PR 2: extend behavior</li>



<li>PR 3: polish</li>
</ul>



<p class="wp-block-paragraph">Each PR:</p>



<ul class="wp-block-list">
<li>delivers something usable</li>



<li>is independently testable</li>



<li>is small</li>
</ul>



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



<h3 class="wp-block-heading">4. Optimize review speed (not just quality)</h3>



<p class="wp-block-paragraph">Set expectations:</p>



<ul class="wp-block-list">
<li>PR should be reviewed within <strong>a few hours</strong></li>



<li>not days</li>
</ul>



<p class="wp-block-paragraph">Ways to achieve this:</p>



<ul class="wp-block-list">
<li>smaller PRs (obviously)</li>



<li>clear ownership</li>



<li>synchronous reviews when needed (pairing)</li>
</ul>



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



<h3 class="wp-block-heading">5. Accept “ugly but correct” code (temporarily)</h3>



<p class="wp-block-paragraph">This is uncomfortable but critical.</p>



<p class="wp-block-paragraph">Sometimes the right move is:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“Merge this small, slightly imperfect change now”</p>
</blockquote>



<p class="wp-block-paragraph">Instead of:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“Wait until it’s perfectly clean”</p>
</blockquote>



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



<p class="wp-block-paragraph">Because flow matters more than perfection.</p>



<p class="wp-block-paragraph">You can always:</p>



<ul class="wp-block-list">
<li>refactor later <strong>(but don&#8217;t forget — it&#8217;s important for code cleanup)</strong></li>



<li>improve incrementally</li>
</ul>



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



<h3 class="wp-block-heading">6. Track your actual behavior</h3>



<p class="wp-block-paragraph">If you want to be serious about this, measure:</p>



<ul class="wp-block-list">
<li>average PR size</li>



<li>PR lifetime</li>



<li>number of merges per day</li>
</ul>



<p class="wp-block-paragraph">Reality check:</p>



<p class="wp-block-paragraph">If your PRs:</p>



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



<li>contain hundreds/thousands of lines</li>
</ul>



<p class="wp-block-paragraph">You are not doing trunk-based development.</p>



<p class="wp-block-paragraph">No matter what your team says.</p>



<p class="has-vivid-cyan-blue-color has-text-color has-link-color has-large-font-size wp-elements-c72aeb2fc09cf880f59a6265c6331af0 wp-block-paragraph">What About Large Architectural Changes?</p>



<p class="wp-block-paragraph">A common question that comes up here is: does this apply when you&#8217;re doing a complete architectural overhaul of a service?</p>



<p class="wp-block-paragraph">The short answer is yes — and there&#8217;s a specific pattern for it called <strong>Branch by Abstraction</strong>.</p>



<p class="wp-block-paragraph">Instead of keeping the new architecture in a separate Git branch for months (and facing a merge nightmare later), you keep both versions in the main branch simultaneously. It feels messy at first. It&#8217;s much safer in practice.</p>



<p class="wp-block-paragraph">Here&#8217;s how it works in combination with feature flags:</p>



<ul class="wp-block-list">
<li>You merge small, incremental pieces of the new architecture daily — never a big bang.</li>



<li>You run <strong>dark launches</strong>: the new code executes in production, but its results are ignored or compared against the old implementation. No user impact, real-world validation.</li>



<li>When confidence is high enough, you flip the flag and the new architecture goes live instantly.</li>
</ul>



<p class="wp-block-paragraph">The two things that make this work:</p>



<p class="wp-block-paragraph"><strong>1. The flag has to exist from the start.</strong> Not added later as an afterthought. If you begin the migration without a flag, you&#8217;ll end up in the same situation as a long-lived branch — just in the main branch instead.</p>



<p class="wp-block-paragraph"><strong>2. The cleanup phase is not optional.</strong> Once the rollout is 100% successful, deleting the old code and removing the flag is not a nice-to-have. It&#8217;s part of the work. Skipping this step is how codebases become impossible to understand.</p>



<p class="wp-block-paragraph">The tradeoff is real: having two architectures in the codebase simultaneously requires more discipline and coordination from the team.</p>



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



<p class="wp-block-paragraph"><strong>Trunk-Based Development pt. 3:</strong> <a href="https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/</a></p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-ef15f16782f3dcde3b943a5d38cf05e7">The Real Mindset Shift</h2>



<p class="wp-block-paragraph">This is the uncomfortable truth:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Small pull requests are not a technical practice.<br>They are a discipline.</p>
</blockquote>



<p class="wp-block-paragraph">They require you to:</p>



<ul class="wp-block-list">
<li>let go of “completeness”</li>



<li>embrace incremental delivery</li>



<li>prioritize flow over polish</li>
</ul>



<p class="wp-block-paragraph">And that’s hard.</p>



<p class="wp-block-paragraph">Because it goes against how most developers naturally think about work.</p>



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



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



<p class="wp-block-paragraph">That 6-month pull request wasn’t an exception.</p>



<p class="wp-block-paragraph">It was just an extreme version of a very common problem.</p>



<p class="wp-block-paragraph">Most teams are operating with:</p>



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



<li>slow feedback</li>



<li>delayed merges</li>
</ul>



<p class="wp-block-paragraph">And then they wonder why delivery feels slow.</p>



<p class="wp-block-paragraph">If you fix just one thing, fix this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Make your pull requests smaller. Radically smaller.</p>
</blockquote>



<p class="wp-block-paragraph">Everything else — faster reviews, fewer bugs, smoother delivery — follows from that.</p>



<p class="wp-block-paragraph">Not the other way around.</p>
<p>The post <a href="https://codecraftdiary.com/2026/04/29/trunk-based-development-your-pull-requests-are-still-too-big/">Trunk-Based Development: Your Pull Requests Are Still Too Big</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/04/29/trunk-based-development-your-pull-requests-are-still-too-big/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Laravel Testing Mistakes That Make Your Tests Useless</title>
		<link>https://codecraftdiary.com/2026/04/18/laravel-testing-mistakes/</link>
					<comments>https://codecraftdiary.com/2026/04/18/laravel-testing-mistakes/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 18 Apr 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[backend]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3236</guid>

					<description><![CDATA[<p>Testing in Laravel can feel straightforward at first. You write a few tests, run php artisan test, see green output, and move on. But here’s the uncomfortable truth: many passing tests don’t actually protect your application. If your tests don’t catch real bugs, they’re not just useless—they give you a false sense of confidence. In [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/04/18/laravel-testing-mistakes/">Laravel Testing Mistakes That Make Your Tests Useless</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Testing in Laravel can feel straightforward at first. You write a few tests, run <code>php artisan test</code>, see green output, and move on. But here’s the uncomfortable truth: <strong>many passing tests don’t actually protect your application</strong>.</p>



<p class="wp-block-paragraph">If your tests don’t catch real bugs, they’re not just useless—they give you a false sense of confidence.</p>



<p class="wp-block-paragraph">In this article, we’ll go through the most common Laravel testing mistakes that quietly break the value of your test suite, along with practical examples and better approaches.</p>



<p class="wp-block-paragraph">You can be also interested in testing database logic <a href="https://codecraftdiary.com/2026/01/03/testing-database-logic-what-to-test-what-to-skip-and-why-it-matters/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/01/03/testing-database-logic-what-to-test-what-to-skip-and-why-it-matters/</a></p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-116a19cf3b5b1c1870aa5ced5a37ec6d">1. Testing Implementation Instead of Behavior</h2>



<p class="wp-block-paragraph">One of the biggest mistakes is writing tests that mirror your code instead of validating what your application actually does.</p>



<h3 class="wp-block-heading">Bad example</h3>



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

    $controller = new UserController($service);
    $controller->store(new Request(&#91;...&#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_it_calls_service_method</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">$service</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">Mockery</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">mock</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">UserService</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: #9CDCFE">$service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">shouldReceive</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;createUser&#39;</span><span style="color: #D4D4D4">)-&gt;</span><span style="color: #DCDCAA">once</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$controller</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">UserController</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$controller</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">store</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Request</span><span style="color: #D4D4D4">(&#91;...&#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 test only checks that a method was called. It doesn’t verify:</p>



<ul class="wp-block-list">
<li>what was created</li>



<li>whether the data is correct</li>



<li>whether anything actually works</li>
</ul>



<h3 class="wp-block-heading">Better approach</h3>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(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_is_created()
{
    $response = $this->post('/users', &#91;
        'name' => 'John Doe',
        'email' => 'john@example.com',
    &#93;);

    $response->assertStatus(201);

    $this->assertDatabaseHas('users', &#91;
        'email' => 'john@example.com',
    &#93;);
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_user_is_created</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/users&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;name&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;John Doe&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;email&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;john@example.com&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertStatus</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">201</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;users&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #CE9178">&#39;email&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;john@example.com&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Focus on <strong>observable behavior</strong>, not internal calls.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-20bfda358f1791274dfb728ef0f73623">2. Overusing Mocks</h2>



<p class="wp-block-paragraph">Mocks are powerful—but overusing them leads to fragile and meaningless tests.</p>



<h3 class="wp-block-heading">Problem</h3>



<p class="wp-block-paragraph">When everything is mocked:</p>



<ul class="wp-block-list">
<li>you’re not testing real integration</li>



<li>your tests pass even if the system is broken</li>
</ul>



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

$response = $this->get('/weather');

$response->assertStatus(200);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">();</span></span>
<span class="line"></span>
<span class="line"><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">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/weather&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertStatus</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">200</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span></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 tells you nothing about:</p>



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



<li>data correctness</li>



<li>edge cases</li>
</ul>



<h3 class="wp-block-heading">Better approach</h3>



<p class="wp-block-paragraph">Mock only what you must (external services), and assert meaningful output:</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>Http::fake([
    '*' => Http::response(&#91;'temp' => 25&#93;, 200),
]);

$response = $this->get('/weather');

$response->assertJson(&#91;
    'temperature' => 25,
&#93;);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fake</span><span style="color: #D4D4D4">([</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;*&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #4EC9B0">Http</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">response</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #CE9178">&#39;temp&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">25</span><span style="color: #D4D4D4">&#93;, </span><span style="color: #B5CEA8">200</span><span style="color: #D4D4D4">),</span></span>
<span class="line"><span style="color: #D4D4D4">]);</span></span>
<span class="line"></span>
<span class="line"><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">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/weather&#39;</span><span style="color: #D4D4D4">);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertJson</span><span style="color: #D4D4D4">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;temperature&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">25</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Rule of thumb:<br><strong>Mock boundaries, not your own logic.</strong></p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-4e9bf9085032b006384e7b4bfffd7133">3. Writing Tests That Always Pass</h2>



<p class="wp-block-paragraph">Some tests are written in a way that they can’t fail—even if the code is broken.</p>



<h3 class="wp-block-heading">Example</h3>



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

    $response->assertStatus(200);
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">test_response_is_ok</span><span style="color: #D4D4D4">()</span></span>
<span class="line"><span style="color: #D4D4D4">{</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$response</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">get</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/users&#39;</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">-&gt;</span><span style="color: #DCDCAA">assertStatus</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">200</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<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 will pass even if:</p>



<ul class="wp-block-list">
<li>the response is empty</li>



<li>the wrong data is returned</li>



<li>business logic is broken</li>
</ul>



<h3 class="wp-block-heading">Better approach</h3>



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



<p class="wp-block-paragraph">Or even better:</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->assertDatabaseCount('users', 3);
</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">assertDatabaseCount</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;users&#39;</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">3</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">Ask yourself:<br><strong>“What bug would this test catch?”</strong><br>If the answer is “none”, rewrite it.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-d7e00516ee1ce5a3806934b4782e8355">4. Ignoring Edge Cases</h2>



<p class="wp-block-paragraph">Most bugs don’t happen in the “happy path”. They happen at the edges.</p>



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



<p class="wp-block-paragraph">Only testing valid input:</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->post('/users', &#91;
    'email' => 'john@example.com',
&#93;);
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/users&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;email&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;john@example.com&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">&#93;);</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">Better approach</h3>



<p class="wp-block-paragraph">Test invalid scenarios:</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->post('/users', &#91;
    'email' => 'not-an-email',
&#93;)->assertSessionHasErrors('email');
</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">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/users&#39;</span><span style="color: #D4D4D4">, &#91;</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #CE9178">&#39;email&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;not-an-email&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">&#93;)-&gt;</span><span style="color: #DCDCAA">assertSessionHasErrors</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;email&#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">Also test:</p>



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



<li>duplicate values</li>



<li>unexpected input</li>
</ul>



<p class="wp-block-paragraph">Good tests try to <strong>break your application</strong>.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-0564602c21119ff45e734cc64c3bab45">5. Testing Too Much in Unit Tests</h2>



<p class="wp-block-paragraph">Unit tests should be fast and focused. But many developers turn them into mini integration tests.</p>



<h3 class="wp-block-heading">Example</h3>



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

    $this->assertDatabaseHas('orders', &#91;...&#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_creation</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: #4EC9B0">OrderService</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">(&#91;...&#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;...&#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 mixes:</p>



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



<li>database layer</li>
</ul>



<h3 class="wp-block-heading">Better approach</h3>



<p class="wp-block-paragraph">Split responsibilities:</p>



<p class="wp-block-paragraph"><strong>Unit test (logic only):</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(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_total_price_is_calculated_correctly()
{
    $total = OrderCalculator::calculate(&#91;100, 200&#93;);

    $this->assertEquals(300, $total);
}
</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_total_price_is_calculated_correctly</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">$total</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">OrderCalculator</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">calculate</span><span style="color: #D4D4D4">(&#91;</span><span style="color: #B5CEA8">100</span><span style="color: #D4D4D4">, </span><span style="color: #B5CEA8">200</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">assertEquals</span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">300</span><span style="color: #D4D4D4">, </span><span style="color: #9CDCFE">$total</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"><strong>Feature test (full flow):</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(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->post('/orders', &#91;...&#93;);

$this->assertDatabaseHas('orders', &#91;...&#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">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;/orders&#39;</span><span style="color: #D4D4D4">, &#91;...&#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">assertDatabaseHas</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;orders&#39;</span><span style="color: #D4D4D4">, &#91;...&#93;);</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Keep your test layers clean.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-28b8fcc56502b0d2502efefd3a25a4bf">6. Not Using Factories Properly</h2>



<p class="wp-block-paragraph">Laravel factories are powerful, but many developers misuse them.</p>



<h3 class="wp-block-heading">Problem</h3>



<p class="wp-block-paragraph">Hardcoding everything:</p>



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



<h3 class="wp-block-heading">Better approach</h3>



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



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



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



<li>more flexible tests</li>



<li>easier maintenance</li>
</ul>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-282fc89e73c7cdc4c36f8c4a6065d196">7. Not Cleaning Up Test Data</h2>



<p class="wp-block-paragraph">Dirty test data can cause flaky tests.</p>



<h3 class="wp-block-heading">Problem</h3>



<p class="wp-block-paragraph">Tests depend on previous state.</p>



<h3 class="wp-block-heading">Solution</h3>



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



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



<p class="wp-block-paragraph">This ensures:</p>



<ul class="wp-block-list">
<li>clean DB for each test</li>



<li>consistent results</li>
</ul>



<p class="wp-block-paragraph">Flaky tests destroy trust in your test suite.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-e2ff149ff8927818a77e3559dac81e56">8. Writing Tests That Are Too Complex</h2>



<p class="wp-block-paragraph">If your test is hard to read, it’s probably doing too much.</p>



<h3 class="wp-block-heading">Example</h3>



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



<p class="wp-block-paragraph">Break it down:</p>



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



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-0fa1d284e222611c880f0f0aa61ae946">9. Ignoring Performance</h2>



<p class="wp-block-paragraph">Slow tests are often skipped—and skipped tests are useless tests.</p>



<h3 class="wp-block-heading">Problem</h3>



<ul class="wp-block-list">
<li>too many DB calls</li>



<li>unnecessary setup</li>



<li>heavy fixtures</li>
</ul>



<h3 class="wp-block-heading">Tips</h3>



<ul class="wp-block-list">
<li>use in-memory database (SQLite)</li>



<li>avoid unnecessary seeding</li>



<li>keep unit tests fast</li>
</ul>



<p class="wp-block-paragraph">Fast tests = tests you actually run.</p>



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



<h2 class="wp-block-heading has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-efaa11ce468c186d8795ae68cc942d0c">10. Not Testing Real User Flows</h2>



<p class="wp-block-paragraph">Testing isolated pieces is not enough.</p>



<h3 class="wp-block-heading">Problem</h3>



<p class="wp-block-paragraph">You test services and controllers separately, but never the full flow.</p>



<h3 class="wp-block-heading">Example</h3>



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

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



<p class="wp-block-paragraph">This is what actually matters:<br><strong>Can the user complete the action?</strong></p>



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



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



<p class="wp-block-paragraph">Laravel makes testing easy—but writing <em>useful</em> tests is a different skill.</p>



<p class="wp-block-paragraph">If your tests:</p>



<ul class="wp-block-list">
<li>only check status codes</li>



<li>mock everything</li>



<li>mirror your implementation</li>
</ul>



<p class="wp-block-paragraph">…then they’re not protecting your application.</p>



<p class="wp-block-paragraph">Instead, focus on:</p>



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



<li>meaningful assertions</li>



<li>edge cases</li>



<li>realistic user flows</li>
</ul>



<p class="wp-block-paragraph">A smaller set of high-quality tests is far more valuable than a large suite of weak ones.</p>



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



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



<p class="wp-block-paragraph">Before committing a test, ask:</p>



<ul class="wp-block-list">
<li>Does this test fail if something important breaks?</li>



<li>Am I testing behavior, not implementation?</li>



<li>Would this catch a real bug?</li>



<li>Is this test simple and readable?</li>
</ul>



<p class="wp-block-paragraph">If the answer is “no”, it’s time to improve it.</p>



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



<p class="wp-block-paragraph">Well-written tests are not just about coverage—they’re about confidence. And confidence comes from tests that actually matter.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/04/18/laravel-testing-mistakes/">Laravel Testing Mistakes That Make Your Tests Useless</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/04/18/laravel-testing-mistakes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Fat Controller Laravel Refactor: Step-by-Step Clean Architecture Guide</title>
		<link>https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/</link>
					<comments>https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 11 Apr 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Refactoring & Patterns]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3227</guid>

					<description><![CDATA[<p>Refactoring a fat controller in Laravel is one of the most impactful improvements you can make in a growing codebase. As projects evolve, controllers often become overloaded with validation, business logic, and side effects, making them difficult to maintain and test. A controller starts small. Clean. Readable. Then features get added. Deadlines hit. Logic piles [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/">Fat Controller Laravel Refactor: Step-by-Step Clean Architecture Guide</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Refactoring a fat controller in Laravel is one of the most impactful improvements you can make in a growing codebase. As projects evolve, controllers often become overloaded with validation, business logic, and side effects, making them difficult to maintain and test.</p>



<p class="wp-block-paragraph">A controller starts small. Clean. Readable.</p>



<p class="wp-block-paragraph">Then features get added.</p>



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



<p class="wp-block-paragraph">Logic piles up.</p>



<p class="wp-block-paragraph">And suddenly, you’re staring at a 500-line controller that handles validation, business logic, database writes, API calls, and maybe even a bit of formatting “just for now.”</p>



<p class="wp-block-paragraph">This is what we call a <strong>Fat Controller</strong> — and it’s one of the most common maintainability problems in Laravel applications.</p>



<p class="wp-block-paragraph">In this article, we’ll take a real-world approach and refactor a fat controller into a cleaner, scalable structure using principles inspired by Clean Architecture.</p>



<p class="wp-block-paragraph">No theory overload. Just practical steps.</p>



<p class="wp-block-paragraph">Previous Article in this category : <a href="https://codecraftdiary.com/2026/03/21/ai-driven-refactoring/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/03/21/ai-driven-refactoring/</a></p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-991fb41291386a605fae1a2dd509fe05" style="color:#0092eb">The Problem: A Real Fat Controller Example</h1>



<p class="wp-block-paragraph">Let’s start with something painfully familiar:</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
{
    public function store(Request $request)
    {
        // Validation
        $validated = $request->validate(&#91;
            'user_id' => 'required|exists:users,id',
            'items' => 'required|array',
        &#93;);

        // Business logic
        $total = 0;

        foreach ($validated&#91;'items'&#93; as $item) {
            $product = Product::find($item&#91;'id'&#93;);

            if (!$product) {
                throw new Exception('Product not found');
            }

            if ($product->stock &lt; $item&#91;'quantity'&#93;) {
                throw new Exception('Not enough stock');
            }

            $total += $product->price * $item&#91;'quantity'&#93;;

            $product->stock -= $item&#91;'quantity'&#93;;
            $product->save();
        }

        // Save order
        $order = Order::create([
            'user_id' => $validated&#91;'user_id'&#93;,
            'total' => $total,
        ]);

        // Save items
        foreach ($validated&#91;'items'&#93; as $item) {
            OrderItem::create([
                'order_id' => $order->id,
                'product_id' => $item&#91;'id'&#93;,
                'quantity' => $item&#91;'quantity'&#93;,
            ]);
        }

        // External API call
        Http::post('https://example.com/webhook', &#91;
            'order_id' => $order->id,
        &#93;);

        return response()->json($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: #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">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">store</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Request</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #D4D4D4">    {</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Validation</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$validated</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">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;required|exists:users,id&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;required|array&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Business logic</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$total</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: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93; as </span><span style="color: #9CDCFE">$item</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">find</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;id&#39;</span><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">$product</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">Exception</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Product not found&#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">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stock</span><span style="color: #D4D4D4"> &lt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;) {</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">Exception</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Not enough stock&#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">$total</span><span style="color: #D4D4D4"> += </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">price</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;;</span></span>
<span class="line"></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">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;;</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>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// Save order</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">create</span><span style="color: #D4D4D4">([</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">$validated</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;total&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$total</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">// Save items</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93; as </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderItem</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">([</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;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4">&#93;,</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: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            ]);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// External API call</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">post</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;https://example.com/webhook&#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">        &#93;);</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">$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></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">What’s wrong here?</h3>



<ul class="wp-block-list">
<li>Controller handles <strong>too many responsibilities</strong></li>



<li>Business logic is <strong>not reusable</strong></li>



<li>Hard to <strong>test</strong></li>



<li>Tight coupling to Eloquent and external APIs</li>



<li>Changes are risky</li>
</ul>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-e47101587334084f3316d73a1a00182e" style="color:#0092eb">Goal: What “Clean” Looks Like</h1>



<p class="wp-block-paragraph">We want to move toward:</p>



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



<li>Isolated business logic</li>



<li>Testable services</li>



<li>Clear boundaries between layers</li>
</ul>



<p class="wp-block-paragraph">We’re not going full academic Clean Architecture. We’re applying <strong>just enough structure to stay sane</strong>.</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-b05d68ecdcfd94e86d88750074971eec" style="color:#0092eb">Step 1: Extract Business Logic into a Service</h1>



<p class="wp-block-paragraph">First, move the core logic out of the controller.</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 CreateOrderService
{
    public function handle(array $data): Order
    {
        $total = 0;

        foreach ($data&#91;'items'&#93; as $item) {
            $product = Product::find($item&#91;'id'&#93;);

            if (!$product) {
                throw new Exception('Product not found');
            }

            if ($product->stock &lt; $item&#91;'quantity'&#93;) {
                throw new Exception('Not enough stock');
            }

            $total += $product->price * $item&#91;'quantity'&#93;;

            $product->stock -= $item&#91;'quantity'&#93;;
            $product->save();
        }

        $order = Order::create([
            'user_id' => $data&#91;'user_id'&#93;,
            'total' => $total,
        ]);

        foreach ($data&#91;'items'&#93; as $item) {
            OrderItem::create([
                'order_id' => $order->id,
                'product_id' => $item&#91;'id'&#93;,
                'quantity' => $item&#91;'quantity'&#93;,
            ]);
        }

        Http::post('https://example.com/webhook', &#91;
            'order_id' => $order->id,
        &#93;);

        return $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: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CreateOrderService</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: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</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">$total</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: #C586C0">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93; as </span><span style="color: #9CDCFE">$item</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">find</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;id&#39;</span><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">$product</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">Exception</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Product not found&#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">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">stock</span><span style="color: #D4D4D4"> &lt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;) {</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">Exception</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;Not enough stock&#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">$total</span><span style="color: #D4D4D4"> += </span><span style="color: #9CDCFE">$product</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #9CDCFE">price</span><span style="color: #D4D4D4"> * </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;;</span></span>
<span class="line"></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">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;;</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>
<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: #4EC9B0">Order</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">([</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">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;total&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$total</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">foreach</span><span style="color: #D4D4D4"> (</span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93; as </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">) {</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #4EC9B0">OrderItem</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">create</span><span style="color: #D4D4D4">([</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;product_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4">&#93;,</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: #9CDCFE">$item</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            ]);</span></span>
<span class="line"><span style="color: #D4D4D4">        }</span></span>
<span class="line"></span>
<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://example.com/webhook&#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">        &#93;);</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 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">Controller becomes:</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
{
    public function store(Request $request, CreateOrderService $service)
    {
        $validated = $request->validate(&#91;
            'user_id' => 'required|exists:users,id',
            'items' => 'required|array',
        &#93;);

        $order = $service->handle($validated);

        return response()->json($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: #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">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">store</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Request</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$request</span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0">CreateOrderService</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$service</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">$validated</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">(&#91;</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;required|exists:users,id&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #CE9178">&#39;required|array&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        &#93;);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$validated</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">$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></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">Improvement:</h3>



<ul class="wp-block-list">
<li>Controller is now thin</li>



<li>Logic is reusable</li>



<li>Easier to test</li>
</ul>



<p class="wp-block-paragraph">But we’re not done yet.</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-8d12f904a0f9b26ed6db0c892e409c2e" style="color:#0092eb">Step 2: Introduce a Data Transfer Object (DTO)</h1>



<p class="wp-block-paragraph">Passing raw arrays is fragile.</p>



<p class="wp-block-paragraph">Let’s fix that.</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 CreateOrderData
{
    public function __construct(
        public int $userId,
        public array $items
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            $data&#91;'user_id'&#93;,
            $data&#91;'items'&#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">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CreateOrderData</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">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">int</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$userId</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$items</span></span>
<span class="line"><span style="color: #D4D4D4">    ) {}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">static</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">fromArray</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">array</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">): </span><span style="color: #569CD6">self</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">new</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">self</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;user_id&#39;</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">            </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">&#91;</span><span style="color: #CE9178">&#39;items&#39;</span><span style="color: #D4D4D4">&#93;</span></span>
<span class="line"><span style="color: #D4D4D4">        );</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span 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">Update controller:</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>$data = CreateOrderData::fromArray($validated);
$order = $service->handle($data);
</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">$data</span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0">CreateOrderData</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">fromArray</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$validated</span><span style="color: #D4D4D4">);</span></span>
<span class="line"><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$data</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">Update service:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(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 handle(CreateOrderData $data): 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: #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">CreateOrderData</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">Order</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">Improvement:</h3>



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



<li>Safer refactoring</li>



<li>Clear contract</li>
</ul>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-2a5dd61e85b550e82acf11ca5c464d2f" style="color:#0092eb">Step 3: Decouple External Dependencies</h1>



<p class="wp-block-paragraph">Right now, your service is tightly coupled to:</p>



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



<li>HTTP client</li>
</ul>



<p class="wp-block-paragraph">Let’s extract the webhook logic.</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 OrderWebhookService
{
    public function send(Order $order): void
    {
        Http::post('https://example.com/webhook', &#91;
            'order_id' => $order->id,
        &#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">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">OrderWebhookService</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">send</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: #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://example.com/webhook&#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">        &#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">    }</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Inject 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 CreateOrderService
{
    public function __construct(
        private OrderWebhookService $webhook
    ) {}

    public function handle(CreateOrderData $data): Order
    {
        // logic...

        $this->webhook->send($order);

        return $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: #569CD6">class</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CreateOrderService</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">OrderWebhookService</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$webhook</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">CreateOrderData</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">$data</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: #6A9955">// logic...</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">webhook</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">send</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: #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></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">Improvement:</h3>



<ul class="wp-block-list">
<li>External side effects are isolated</li>



<li>Easier to mock in tests</li>
</ul>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-e473ee78c02ca54fb8bbff3263948cbd" style="color:#0092eb">Step 4: Make It Testable</h1>



<p class="wp-block-paragraph">Now you can test the service independently:</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_it_creates_order()
{
    $service = app(CreateOrderService::class);

    $data = new CreateOrderData(
        userId: 1,
        items: [
            &#91;'id' => 1, 'quantity' => 2&#93;,
        ]
    );

    $order = $service->handle($data);

    $this->assertNotNull($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_it_creates_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 style="color: #9CDCFE">$service</span><span style="color: #D4D4D4"> = </span><span style="color: #DCDCAA">app</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">CreateOrderService</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: #9CDCFE">$data</span><span style="color: #D4D4D4"> = </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">CreateOrderData</span><span style="color: #D4D4D4">(</span></span>
<span class="line"><span style="color: #D4D4D4">        userId: </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">        items: [</span></span>
<span class="line"><span style="color: #D4D4D4">            &#91;</span><span style="color: #CE9178">&#39;id&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">1</span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">&#39;quantity&#39;</span><span style="color: #D4D4D4"> =&gt; </span><span style="color: #B5CEA8">2</span><span style="color: #D4D4D4">&#93;,</span></span>
<span class="line"><span style="color: #D4D4D4">        ]</span></span>
<span class="line"><span style="color: #D4D4D4">    );</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4"> = </span><span style="color: #9CDCFE">$service</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">handle</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$data</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">assertNotNull</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></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">Before refactoring, this would require:</p>



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



<li>Controller testing</li>



<li>Complex setup</li>
</ul>



<p class="wp-block-paragraph">Now it’s isolated.</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-f071ab6b974484e9595c4a2b5f5a6fd2" style="color:#0092eb">Step 5: Optional — Introduce Repositories (Only If Needed)</h1>



<p class="wp-block-paragraph">Don’t overengineer this.</p>



<p class="wp-block-paragraph">But if your app grows, you might extract:</p>



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



<h3 class="wp-block-heading">When to do this:</h3>



<ul class="wp-block-list">
<li>Multiple data sources</li>



<li>Complex queries</li>



<li>Domain logic reuse</li>
</ul>



<h3 class="wp-block-heading">When NOT to:</h3>



<ul class="wp-block-list">
<li>Simple CRUD apps</li>
</ul>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-9c42f58011d4e629a65d0596dbcd328d" style="color:#0092eb">Before vs After</h1>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Aspect</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Controller size</td><td>Huge</td><td>Minimal</td></tr><tr><td>Testability</td><td>Hard</td><td>Easy</td></tr><tr><td>Reusability</td><td>None</td><td>High</td></tr><tr><td>Coupling</td><td>High</td><td>Reduced</td></tr><tr><td>Maintainability</td><td>Painful</td><td>Scalable</td></tr></tbody></table></figure>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-7642e60fdb05c923f655fe5d8071a27d" style="color:#0092eb">Common Mistakes to Avoid</h1>



<h3 class="wp-block-heading">1. Moving everything blindly into services</h3>



<p class="wp-block-paragraph">You’ll just create <strong>fat services instead of fat controllers</strong>.</p>



<p class="wp-block-paragraph">Keep services focused.</p>



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



<h3 class="wp-block-heading">2. Overengineering with too many layers</h3>



<p class="wp-block-paragraph">You don’t need:</p>



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



<li>5 abstractions</li>



<li>enterprise architecture™</li>
</ul>



<p class="wp-block-paragraph">Start simple. Evolve when needed.</p>



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



<h3 class="wp-block-heading">3. Ignoring boundaries</h3>



<p class="wp-block-paragraph">Controllers = HTTP<br>Services = business logic<br>Models = persistence</p>



<p class="wp-block-paragraph">Mixing these again = back to chaos.</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-8debc7dd1b41b90e3f47eecd871a3323" style="color:#0092eb">Key Takeaways</h1>



<ul class="wp-block-list">
<li>Fat controllers are a <strong>symptom</strong>, not the root problem</li>



<li>The real issue is <strong>mixed responsibilities</strong></li>



<li>Start with <strong>service extraction</strong></li>



<li>Add DTOs for safety</li>



<li>Isolate side effects (APIs, events)</li>



<li>Only introduce more abstraction when justified</li>
</ul>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-129cdfa1bc4360e7ce866fb1cfc8eebc" style="color:#0092eb">Final Thought</h1>



<p class="wp-block-paragraph">Clean Architecture in Laravel doesn’t mean rewriting your app into a textbook diagram.</p>



<p class="wp-block-paragraph">It means one thing:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Making your code easier to change without fear.</p>
</blockquote>



<p class="wp-block-paragraph">And the fastest way to get there?</p>



<p class="wp-block-paragraph">Start killing your fat controllers — one method at a time.</p>



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



<p class="wp-block-paragraph">If this is something you’re dealing with right now, your next step is simple:</p>



<p class="wp-block-paragraph">Pick your worst controller and extract just one action into a service.</p>



<p class="wp-block-paragraph">That’s how clean architecture actually starts.</p>
<p>The post <a href="https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/">Fat Controller Laravel Refactor: Step-by-Step Clean Architecture Guide</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Trunk-Based Development: Why Most Teams Think They Use It (But Don’t)</title>
		<link>https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/</link>
					<comments>https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/#comments</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 04 Apr 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Development Workflow & Best Practices]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[workflow]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3222</guid>

					<description><![CDATA[<p>Trunk-Based Development sounds simple. No long-lived branches.Frequent merges.Small, incremental changes. Most teams will tell you: “Yeah, we basically do trunk-based development.” In practice, they don’t. What they usually have is a hybrid that keeps the downsides of feature branches — while pretending to get the benefits of trunk-based development. I’ve seen this pattern in multiple [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/">Trunk-Based Development: Why Most Teams Think They Use It (But Don’t)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Trunk-Based Development sounds simple.</p>



<p class="wp-block-paragraph">No long-lived branches.<br>Frequent merges.<br>Small, incremental changes.</p>



<p class="wp-block-paragraph">Most teams will tell you:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“Yeah, we basically do trunk-based development.”</p>
</blockquote>



<p class="wp-block-paragraph">In practice, they don’t.</p>



<p class="wp-block-paragraph">What they usually have is a hybrid that keeps the downsides of feature branches — while pretending to get the benefits of trunk-based development.</p>



<p class="wp-block-paragraph">I’ve seen this pattern in multiple backend teams. On paper, everything looks modern. In reality, delivery is still slow, pull requests are large, and integration is painful.</p>



<p class="wp-block-paragraph">So let’s break down what’s actually going on.</p>



<p class="wp-block-paragraph">Last article in this category: <a href="https://codecraftdiary.com/2026/03/15/small-pull-requests/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/03/15/small-pull-requests/</a></p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-e5703807871822d47e4cf731396df980" style="color:#0092eb">The Illusion of Trunk-Based Development</h1>



<p class="wp-block-paragraph">Ask a team how they work, and you’ll often hear something like:</p>



<ul class="wp-block-list">
<li>“We merge to main frequently”</li>



<li>“We don’t keep branches for too long”</li>



<li>“We try to keep PRs small”</li>
</ul>



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



<p class="wp-block-paragraph">But then you look closer:</p>



<ul class="wp-block-list">
<li>PRs are open for 3–5 days</li>



<li>branches still contain multiple features</li>



<li>merges are delayed because reviews are slow</li>



<li>developers are afraid to merge unfinished work</li>
</ul>



<p class="wp-block-paragraph">This is not trunk-based development.</p>



<p class="wp-block-paragraph">This is just <strong>shorter feature branches</strong>.</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-10620f000e32dcee9567d869095bfec6" style="color:#0092eb">What Real Trunk-Based Development Actually Requires</h1>



<p class="wp-block-paragraph">Trunk-based development is not about branches.</p>



<p class="wp-block-paragraph">It’s about <strong>integration frequency and safety</strong>.</p>



<p class="wp-block-paragraph">At its core, it requires:</p>



<ul class="wp-block-list">
<li>merging to main at least daily (ideally multiple times per day)</li>



<li>keeping changes small enough to review quickly</li>



<li>having strong safety mechanisms in place</li>
</ul>



<p class="wp-block-paragraph">Without these, trunk-based development collapses.</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-d245389f0bd2c45912ec8b8b2b755187" style="color:#0092eb">Where Most Teams Break</h1>



<h2 class="wp-block-heading">1. Pull Requests Are Still Too Big</h2>



<p class="wp-block-paragraph">This is the biggest issue.</p>



<p class="wp-block-paragraph">A developer starts a “small” feature:</p>



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



<li>updates service</li>



<li>modifies database</li>



<li>adds validation</li>



<li>fixes something unrelated</li>
</ul>



<p class="wp-block-paragraph">Now the PR has:</p>



<ul class="wp-block-list">
<li>15 files changed</li>



<li>600+ lines</li>



<li>multiple concerns</li>
</ul>



<p class="wp-block-paragraph">Review slows down. Feedback increases. Merge is delayed.</p>



<p class="wp-block-paragraph">At that point, it doesn’t matter what you call your workflow —<br>you’re not doing trunk-based development.</p>



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



<h2 class="wp-block-heading">2. Code Reviews Block Integration</h2>



<p class="wp-block-paragraph">In theory:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“We merge frequently”</p>
</blockquote>



<p class="wp-block-paragraph">In reality:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“We merge when the PR gets approved”</p>
</blockquote>



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



<p class="wp-block-paragraph">If reviews take:</p>



<ul class="wp-block-list">
<li>1 day → integration is delayed</li>



<li>2–3 days → conflicts increase</li>



<li>5 days → context is lost</li>
</ul>



<p class="wp-block-paragraph">Now developers start stacking changes on top of each other.</p>



<p class="wp-block-paragraph">And suddenly:</p>



<ul class="wp-block-list">
<li>branches live longer</li>



<li>PRs get bigger</li>



<li>merges become risky</li>
</ul>



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



<h2 class="wp-block-heading">3. Teams Are Afraid to Merge Incomplete Work</h2>



<p class="wp-block-paragraph">This is subtle but important.</p>



<p class="wp-block-paragraph">Developers often think:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“I can’t merge this yet — it’s not finished”</p>
</blockquote>



<p class="wp-block-paragraph">So they keep working on the branch.</p>



<p class="wp-block-paragraph">The problem:</p>



<ul class="wp-block-list">
<li>the longer the branch lives</li>



<li>the more it diverges</li>



<li>the harder it is to merge</li>
</ul>



<p class="wp-block-paragraph">Trunk-based development requires a different mindset:</p>



<p class="wp-block-paragraph">You merge incomplete work safely.</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-6649ee8814ec19eed42444fcc334b851" style="color:#0092eb">The Missing Piece: Feature Flags</h1>



<p class="wp-block-paragraph">Most teams skip this.</p>



<p class="wp-block-paragraph">And without it, trunk-based development doesn’t work.</p>



<h3 class="wp-block-heading">Example</h3>



<p class="wp-block-paragraph">You’re building a new payment flow.</p>



<p class="wp-block-paragraph">Without feature flags:</p>



<ul class="wp-block-list">
<li>you need to finish everything before merging</li>



<li>you keep a long-lived branch</li>



<li>integration is delayed</li>
</ul>



<p class="wp-block-paragraph">With feature 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(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>if (Feature::enabled('new_payment_flow')) {
    $this->newFlow();
} else {
    $this->oldFlow();
}
</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: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #4EC9B0">Feature</span><span style="color: #D4D4D4">::</span><span style="color: #DCDCAA">enabled</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&#39;new_payment_flow&#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">newFlow</span><span style="color: #D4D4D4">();</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: #569CD6">$this</span><span style="color: #D4D4D4">-&gt;</span><span style="color: #DCDCAA">oldFlow</span><span style="color: #D4D4D4">();</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">Now you can:</p>



<ul class="wp-block-list">
<li>merge partial work</li>



<li>deploy continuously</li>



<li>control exposure</li>
</ul>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-933388475fdebcc1aa2ed5cd727e37dd" style="color:#0092eb">Real Example From Practice</h1>



<p class="wp-block-paragraph">I worked with a team that claimed they were doing trunk-based development.</p>



<p class="wp-block-paragraph">Metrics looked like this:</p>



<ul class="wp-block-list">
<li>average PR size: ~500 lines</li>



<li>review time: 2–3 days</li>



<li>merges per developer: ~2 per week</li>
</ul>



<p class="wp-block-paragraph">After digging in, the issue was clear:</p>



<ul class="wp-block-list">
<li>developers grouped work “to be efficient”</li>



<li>reviews were asynchronous and slow</li>



<li>no feature flags</li>
</ul>



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



<h3 class="wp-block-heading">What changed</h3>



<p class="wp-block-paragraph">We introduced 3 rules:</p>



<ol class="wp-block-list">
<li>PR must be mergeable within the same day</li>



<li>no PR over ~300 lines (soft limit)</li>



<li>feature flags for incomplete features</li>
</ol>



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



<h3 class="wp-block-heading">Result after ~3 weeks:</h3>



<ul class="wp-block-list">
<li>PR size dropped by ~40%</li>



<li>review time dropped to hours</li>



<li>merges increased to multiple per day</li>



<li>production issues decreased</li>
</ul>



<p class="wp-block-paragraph">Not because developers got better.</p>



<p class="wp-block-paragraph">Because the system changed.</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-109682028e39434a34e60100cc836c25" style="color:#0092eb">The Hidden Constraint: CI/CD Speed</h1>



<p class="wp-block-paragraph">Here’s something teams often ignore:</p>



<p class="wp-block-paragraph">You cannot do trunk-based development with slow pipelines.</p>



<p class="wp-block-paragraph">If your CI takes:</p>



<ul class="wp-block-list">
<li>20 minutes → friction</li>



<li>40 minutes → developers wait</li>



<li>60+ minutes → people stop merging frequently</li>
</ul>



<p class="wp-block-paragraph">So what happens?</p>



<ul class="wp-block-list">
<li>developers batch changes</li>



<li>PRs get bigger</li>



<li>integration slows down</li>
</ul>



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



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-6fb1273a52543b70a0bd6271ffea68fd" style="color:#0092eb">Rule of thumb (2026 reality):</h3>



<ul class="wp-block-list">
<li>CI under 10 minutes → good</li>



<li>under 5 minutes → ideal</li>
</ul>



<p class="wp-block-paragraph">Anything above that:<br>→ you’re actively fighting your workflow</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-ad88f6df73721dbf0a4fc8fa61593aa1" style="color:#0092eb">Why This Matters More in 2026</h1>



<p class="wp-block-paragraph">With AI-assisted coding, developers can generate code faster than ever.</p>



<p class="wp-block-paragraph">That creates a new problem:</p>



<p class="wp-block-paragraph"><strong>volume of changes increases</strong></p>



<p class="wp-block-paragraph"><strong>but review capacity doesn’t</strong></p>



<p class="wp-block-paragraph">If you don’t enforce:</p>



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



<li>fast integration</li>



<li>clear boundaries</li>
</ul>



<p class="wp-block-paragraph">your workflow collapses under its own weight.</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-414578f574201477fe295a4ea10c5753" style="color:#0092eb">How to Tell If You’re Actually Doing It</h1>



<p class="wp-block-paragraph">Be honest and check:</p>



<ul class="wp-block-list">
<li>Do you merge to main multiple times per day?</li>



<li>Are most PRs reviewed within hours, not days?</li>



<li>Can you safely merge incomplete work?</li>



<li>Are branches short-lived (hours, not days)?</li>
</ul>



<p class="wp-block-paragraph">If not:<br>→ you’re not doing trunk-based development</p>



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-a7885f2e9c5b39e916fd3b30c633caf1" style="color:#0092eb">Practical Rules You Can Apply Tomorrow</h1>



<p class="wp-block-paragraph">If you want to move closer to real trunk-based development, start here:</p>



<h3 class="wp-block-heading">1. Limit PR size</h3>



<p class="wp-block-paragraph">Not as a guideline — as a rule.</p>



<h3 class="wp-block-heading">2. Optimize for review speed</h3>



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



<li>clearer intent</li>



<li>less context switching</li>
</ul>



<h3 class="wp-block-heading">3. Introduce feature flags</h3>



<p class="wp-block-paragraph">Without them, you’re stuck.</p>



<h3 class="wp-block-heading">4. Fix your CI/CD bottlenecks</h3>



<p class="wp-block-paragraph">Speed is not a luxury — it’s a requirement.</p>



<h3 class="wp-block-heading">5. Stop batching work</h3>



<p class="wp-block-paragraph">Batching feels efficient.<br>It’s not.</p>



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



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



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



<h1 class="wp-block-heading has-text-color has-link-color wp-elements-5740294cf3ff6bd8aea101de05485833" style="color:#0092eb">Closing Thought</h1>



<p class="wp-block-paragraph">Trunk-based development is not a branching strategy.</p>



<p class="wp-block-paragraph">It’s a discipline.</p>



<p class="wp-block-paragraph">Most teams don’t fail because they don’t understand it.<br>They fail because they don’t change the constraints that make it possible.</p>



<p class="wp-block-paragraph">If your:</p>



<ul class="wp-block-list">
<li>PRs are big</li>



<li>reviews are slow</li>



<li>CI is slow</li>



<li>merges are risky</li>
</ul>



<p class="wp-block-paragraph">then it doesn’t matter what you call your workflow.</p>



<p class="wp-block-paragraph">You’re still doing feature-branch development — just with better branding.</p>



<p class="wp-block-paragraph">And that’s exactly why your delivery still feels slower than it should.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/">Trunk-Based Development: Why Most Teams Think They Use It (But Don’t)</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Laravel RefreshDatabase vs DatabaseTransactions: When to Use Each</title>
		<link>https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/</link>
					<comments>https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/#respond</comments>
		
		<dc:creator><![CDATA[codecraftdiary]]></dc:creator>
		<pubDate>Sat, 28 Mar 2026 09:26:07 +0000</pubDate>
				<category><![CDATA[Testing]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[laravel]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://codecraftdiary.com/?p=3217</guid>

					<description><![CDATA[<p>Laravel RefreshDatabase vs DatabaseTransactions is one of the most common sources of confusion when writing tests in Laravel. Choosing the wrong approach can lead to flaky tests, hidden bugs, and unreliable results. When writing tests in Laravel, database state can quickly become a source of confusion. One test passes, another fails. Data seems to “leak” [&#8230;]</p>
<p>The post <a href="https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/">Laravel RefreshDatabase vs DatabaseTransactions: When to Use Each</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Laravel RefreshDatabase vs DatabaseTransactions is one of the most common sources of confusion when writing tests in Laravel.</p>



<p class="wp-block-paragraph">Choosing the wrong approach can lead to flaky tests, hidden bugs, and unreliable results.</p>



<p class="wp-block-paragraph">When writing tests in Laravel, database state can quickly become a source of confusion.</p>



<p class="wp-block-paragraph">One test passes, another fails. Data seems to “leak” between tests. Records appear when they shouldn’t — or disappear when you expect them to exist.</p>



<p class="wp-block-paragraph">If you’ve experienced this, you’re not alone.</p>



<p class="wp-block-paragraph">In most cases, the issue comes down to how your tests handle the database. Laravel provides two primary approaches for this:</p>



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



<li><code>DatabaseTransactions</code></li>
</ul>



<p class="wp-block-paragraph">At first glance, they seem similar. In reality, they behave very differently — and choosing the wrong one can lead to subtle bugs, flaky tests, or even false confidence in your test suite.</p>



<p class="wp-block-paragraph">In this guide, you’ll learn exactly how both approaches work, their trade-offs, and when to use each in real-world scenarios.</p>



<p class="wp-block-paragraph">Last article in this category: <a href="https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/</a></p>



<p class="wp-block-paragraph">PHP Unit docs: <a href="https://phpunit.de/documentation.html" target="_blank" rel="noreferrer noopener">https://phpunit.de/documentation.html</a></p>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-c6e218a0c97a35374efcf4f8efaa547b" style="color:#0092eb">Understanding the Core Problem</h2>



<p class="wp-block-paragraph">Before diving into Laravel specifics, let’s clarify the core issue:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Tests must be isolated.</p>
</blockquote>



<p class="wp-block-paragraph">Each test should run independently, without being affected by previous tests.</p>



<p class="wp-block-paragraph">If your database is not reset properly between tests:</p>



<ul class="wp-block-list">
<li>data can persist unexpectedly</li>



<li>test results become unreliable</li>



<li>debugging becomes painful</li>
</ul>



<p class="wp-block-paragraph">Laravel solves this problem using two strategies:</p>



<ol class="wp-block-list">
<li>Reset the database completely</li>



<li>Wrap each test in a transaction and roll it back</li>
</ol>



<p class="wp-block-paragraph"><strong>Article about practice feature testing in PHP:</strong> <a href="https://codecraftdiary.com/2025/10/30/feature-testing-in-php-ensuring-the-whole-system-works-together/" target="_blank" rel="noreferrer noopener">https://codecraftdiary.com/2025/10/30/feature-testing-in-php-ensuring-the-whole-system-works-together/</a></p>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-ade6712a79372d81dabc43ea731180b0" style="color:#0092eb">Option 1: RefreshDatabase</h2>



<p class="wp-block-paragraph">The <code>RefreshDatabase</code> trait is the most commonly used solution.</p>



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

class UserTest extends TestCase
{
    use RefreshDatabase;
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Foundation\Testing\</span><span style="color: #4EC9B0">RefreshDatabase</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">UserTest</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 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-text-color has-link-color wp-elements-05ef3242520ca2feef45f6564ae02e64" style="color:#0092eb">How it works</h3>



<ul class="wp-block-list">
<li>Runs migrations before tests</li>



<li>Ensures a clean database state</li>



<li>Uses transactions internally when possible</li>
</ul>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-5f38fe276422fcbf8648d34166b9618e" style="color:#0092eb">What this means in practice</h3>



<p class="wp-block-paragraph">Every test starts with:</p>



<ul class="wp-block-list">
<li>a fresh schema</li>



<li>no leftover data</li>



<li>consistent environment</li>
</ul>



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



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-023549e5f6f504bf423c5d1a979daebc" style="color:#0092eb">Advantages</h3>



<p class="wp-block-paragraph"><strong>1. High reliability</strong></p>



<p class="wp-block-paragraph">Each test runs in a completely clean environment.</p>



<p class="wp-block-paragraph"><strong>2. Works with everything</strong></p>



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



<li>jobs</li>



<li>events</li>



<li>external processes</li>
</ul>



<p class="wp-block-paragraph"><strong>3. Predictable behavior</strong></p>



<p class="wp-block-paragraph">No surprises caused by hidden state.</p>



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



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-2fb46e6663a94f5381e433bbbbde178e" style="color:#0092eb">Disadvantages</h3>



<p class="wp-block-paragraph"><strong>1. Slower</strong></p>



<p class="wp-block-paragraph">Running migrations repeatedly can add overhead.</p>



<p class="wp-block-paragraph"><strong>2. Heavier setup</strong></p>



<p class="wp-block-paragraph">Especially noticeable in large applications.</p>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-79c469f983c5a90a21512800fcf8011d" style="color:#0092eb">Option 2: DatabaseTransactions</h2>



<p class="wp-block-paragraph">The <code>DatabaseTransactions</code> trait takes a different approach.</p>



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

class UserTest extends TestCase
{
    use DatabaseTransactions;
}
</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #569CD6">use</span><span style="color: #D4D4D4"> Illuminate\Foundation\Testing\</span><span style="color: #4EC9B0">DatabaseTransactions</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">UserTest</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">DatabaseTransactions</span><span style="color: #D4D4D4">;</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-413a28475af66eafbd8a06b635a9d87f" style="color:#0092eb">How it works</h3>



<ul class="wp-block-list">
<li>Starts a database transaction before each test</li>



<li>Rolls it back after the test finishes</li>
</ul>



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-2c950b4ef6a692be73f837b5323c6df1" style="color:#0092eb">What this means</h3>



<p class="wp-block-paragraph">Instead of resetting the database:</p>



<ul class="wp-block-list">
<li>changes are never committed</li>



<li>everything is undone automatically</li>
</ul>



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



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-dd7684807916e331d119b65a0cbd781b" style="color:#0092eb">Advantages</h3>



<p class="wp-block-paragraph"><strong>1. Fast</strong></p>



<p class="wp-block-paragraph">Transactions are significantly quicker than migrations.</p>



<p class="wp-block-paragraph"><strong>2. Lightweight</strong></p>



<p class="wp-block-paragraph">No need to rebuild the database schema.</p>



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



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-55126bd221bc861e06d1affdd86f83b2" style="color:#0092eb">Disadvantages</h3>



<p class="wp-block-paragraph"><strong>1. Limited scope</strong></p>



<p class="wp-block-paragraph">Does NOT work well with:</p>



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



<li>async processes</li>



<li>separate DB connections</li>
</ul>



<p class="wp-block-paragraph"><strong>2. Hidden pitfalls</strong></p>



<p class="wp-block-paragraph">If something runs outside the transaction:</p>



<ul class="wp-block-list">
<li>it won’t be rolled back</li>



<li>tests may behave inconsistently</li>
</ul>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-1215ad144b1b6eb53b80ca2765316a7e" style="color:#0092eb">Key Differences</h2>



<p class="wp-block-paragraph">Here’s a clear comparison:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Feature</th><th>RefreshDatabase</th><th>DatabaseTransactions</th></tr></thead><tbody><tr><td>Speed</td><td>Slower</td><td>Faster</td></tr><tr><td>Isolation</td><td>High</td><td>Medium</td></tr><tr><td>Works with queues</td><td>Yes</td><td>No</td></tr><tr><td>Works across connections</td><td>Yes</td><td>No</td></tr><tr><td>Setup complexity</td><td>Medium</td><td>Low</td></tr></tbody></table></figure>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-a221f1330efe31d2991ce26b6acb32bd" style="color:#0092eb">When to Use RefreshDatabase</h2>



<p class="wp-block-paragraph">Use <code>RefreshDatabase</code> when:</p>



<h3 class="wp-block-heading">1. You are testing queues or jobs</h3>



<p class="wp-block-paragraph">Jobs often run outside the main transaction.</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-bottom-disabled cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-Roboto-Mono.ttf" style="font-size:1rem;font-family:Code-Pro-Roboto-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:calc(1 * 0.6 * 1rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#1E1E1E"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>dispatch(new ProcessOrderJob($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: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ProcessOrderJob</span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE">$order</span><span style="color: #D4D4D4">));</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<p class="wp-block-paragraph">If you use <code>DatabaseTransactions</code>, the job may not see the data at all.</p>



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



<h3 class="wp-block-heading">2. You rely on multiple database connections</h3>



<p class="wp-block-paragraph">Transactions don’t span across connections reliably.</p>



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



<h3 class="wp-block-heading">3. You want maximum reliability</h3>



<p class="wp-block-paragraph">If your priority is correctness over speed, this is the safer option.</p>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-4e55e01a3bba0b1876da3f9a41c8d234" style="color:#0092eb">When to Use DatabaseTransactions</h2>



<p class="wp-block-paragraph">Use <code>DatabaseTransactions</code> when:</p>



<h3 class="wp-block-heading">1. You are testing simple database interactions</h3>



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



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



<li>basic CRUD operations</li>
</ul>



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



<h3 class="wp-block-heading">2. Performance matters</h3>



<p class="wp-block-paragraph">In large test suites, this can significantly reduce execution time.</p>



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



<h3 class="wp-block-heading">3. Everything runs in a single request lifecycle</h3>



<p class="wp-block-paragraph">No queues, no async behavior, no external processes.</p>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-4376598e5350657cfe30bbd20fea34d7" style="color:#0092eb">Common Mistakes (and How to Avoid Them)</h2>



<h3 class="wp-block-heading">Mixing both traits</h3>



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



<p class="wp-block-paragraph">This creates unpredictable behavior.</p>



<p class="wp-block-paragraph"><strong>Fix:</strong> Choose one strategy per test class.</p>



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



<h3 class="wp-block-heading">Using transactions with queues</h3>



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



<p class="wp-block-paragraph">Even with fakes, underlying behavior may break if data isn’t committed.</p>



<p class="wp-block-paragraph"><strong>Fix:</strong> Use <code>RefreshDatabase</code> for queue-related tests.</p>



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



<h3 class="wp-block-heading">Assuming transactions isolate everything</h3>



<p class="wp-block-paragraph">They don’t.</p>



<p class="wp-block-paragraph">Anything outside the transaction:</p>



<ul class="wp-block-list">
<li>won’t be rolled back</li>



<li>can pollute your test environment</li>
</ul>



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



<h3 class="wp-block-heading">Ignoring test failures caused by state</h3>



<p class="wp-block-paragraph">Flaky tests often indicate:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">improper database isolation</p>
</blockquote>



<p class="wp-block-paragraph">Don’t ignore them — fix the root cause.</p>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-c87de7047cbba7b381ba62bc6125ccc9" style="color:#0092eb">Real-World Example</h2>



<p class="wp-block-paragraph">Let’s say you’re testing order processing.</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_is_processed()
{
    $order = Order::factory()->create();

    dispatch(new ProcessOrderJob($order));

    $this->assertDatabaseHas('orders', &#91;
        'id' => $order->id,
        'status' => 'processed',
    &#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_is_processed</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: #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: #DCDCAA">dispatch</span><span style="color: #D4D4D4">(</span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">ProcessOrderJob</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">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;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;processed&#39;</span><span style="color: #D4D4D4">,</span></span>
<span class="line"><span style="color: #D4D4D4">    &#93;);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span>
<span class="line"></span></code></pre><span style="display:flex;align-items:flex-end;padding:10px;width:100%;justify-content:flex-start;background-color:#1E1E1E;color:#c7c7c7;font-size:12px;line-height:1;position:relative">PHP</span></div>



<h3 class="wp-block-heading">With DatabaseTransactions</h3>



<ul class="wp-block-list">
<li>job may not see the order</li>



<li>test fails unexpectedly</li>
</ul>



<h3 class="wp-block-heading">With RefreshDatabase</h3>



<ul class="wp-block-list">
<li>data is committed</li>



<li>job works correctly</li>



<li>test passes</li>
</ul>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-6255e82282614954f02fb30b92e2a772" style="color:#0092eb">Recommended Strategy</h2>



<p class="wp-block-paragraph">For most Laravel applications, the safest default is:</p>



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



<p class="wp-block-paragraph">Then optimize later if needed.</p>



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



<h3 class="wp-block-heading has-text-color has-link-color wp-elements-157cc1e9415d94989fe4813f564732d5" style="color:#0092eb">Hybrid approach (advanced)</h3>



<p class="wp-block-paragraph">You can:</p>



<ul class="wp-block-list">
<li>use <code>RefreshDatabase</code> for feature tests</li>



<li>use <code>DatabaseTransactions</code> for isolated unit-like tests</li>
</ul>



<p class="wp-block-paragraph">This gives you:</p>



<ul class="wp-block-list">
<li>reliability where needed</li>



<li>speed where possible</li>
</ul>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-c114ec3af8a7e3501b1ba99c39788434" style="color:#0092eb">Final Thoughts</h2>



<p class="wp-block-paragraph">Database testing in Laravel isn’t just about writing assertions — it’s about controlling state.</p>



<p class="wp-block-paragraph">Choosing between <code>RefreshDatabase</code> and <code>DatabaseTransactions</code> directly affects:</p>



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



<li>debugging time</li>



<li>overall confidence in your application</li>
</ul>



<p class="wp-block-paragraph">If you’re unsure which one to use, start with:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><code>RefreshDatabase</code></p>
</blockquote>



<p class="wp-block-paragraph">It may be slightly slower, but it will save you hours of debugging and prevent subtle, hard-to-track issues.</p>



<p class="wp-block-paragraph">Once your test suite grows, you can selectively introduce <code>DatabaseTransactions</code> where performance matters and constraints are well understood.</p>



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



<h2 class="wp-block-heading has-text-color has-link-color wp-elements-f4f5589d16b432ea58c9464c316b3660" style="color:#0092eb">TL;DR</h2>



<ul class="wp-block-list">
<li>Use <strong>RefreshDatabase</strong> for reliability and real-world scenarios</li>



<li>Use <strong>DatabaseTransactions</strong> for speed in simple cases</li>



<li>Avoid mixing both</li>



<li>Be mindful of queues and async behavior</li>
</ul>



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



<p class="wp-block-paragraph">A well-structured test suite isn’t just about coverage — it’s about trust.</p>



<p class="wp-block-paragraph">And that trust starts with a clean, predictable database.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/">Laravel RefreshDatabase vs DatabaseTransactions: When to Use Each</a> appeared first on <a href="https://codecraftdiary.com">CodeCraft Diary</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
