<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CodeCraft Diary</title>
    <description>The latest articles on DEV Community by CodeCraft Diary (@codecraft_diary_3d13677fb).</description>
    <link>https://dev.to/codecraft_diary_3d13677fb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3572568%2F9b328bfe-d229-4c84-8915-9af499c7bff0.png</url>
      <title>DEV Community: CodeCraft Diary</title>
      <link>https://dev.to/codecraft_diary_3d13677fb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codecraft_diary_3d13677fb"/>
    <language>en</language>
    <item>
      <title>Merge Conflicts Are a Process Problem, Not a Git Problem</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:31:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/merge-conflicts-are-a-process-problem-not-a-git-problem-4m9p</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/merge-conflicts-are-a-process-problem-not-a-git-problem-4m9p</guid>
      <description>&lt;p&gt;Most developers treat merge conflicts as a Git problem.&lt;/p&gt;

&lt;p&gt;Git tells you that two people changed the same code, highlights the conflicting lines, and leaves you to sort it out. After a few minutes of editing, you commit the resolution and move on.&lt;/p&gt;

&lt;p&gt;It feels like Git is the problem.&lt;/p&gt;

&lt;p&gt;But after working on enough backend projects, I've come to a different conclusion.&lt;/p&gt;

&lt;p&gt;Merge conflicts are rarely caused by Git itself.&lt;/p&gt;

&lt;p&gt;Git is simply exposing a problem that already exists in your development process.&lt;/p&gt;

&lt;p&gt;If your team spends hours every week resolving merge conflicts, the real issue probably isn't your version control system. It's how your team works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git Isn't Creating the Conflict
&lt;/h2&gt;

&lt;p&gt;One thing that's easy to forget is that Git doesn't invent conflicts.&lt;/p&gt;

&lt;p&gt;A merge conflict only appears when Git genuinely cannot determine which change should win.&lt;/p&gt;

&lt;p&gt;Imagine two developers working on the same controller.&lt;/p&gt;

&lt;p&gt;Developer A adds request validation.&lt;/p&gt;

&lt;p&gt;Developer B changes the response format.&lt;/p&gt;

&lt;p&gt;Both modify the same method.&lt;/p&gt;

&lt;p&gt;Git stops and asks someone to make the decision because it simply doesn't have enough context.&lt;/p&gt;

&lt;p&gt;That's exactly what it's supposed to do.&lt;/p&gt;

&lt;p&gt;The conflict isn't a Git failure.&lt;/p&gt;

&lt;p&gt;The failure happened much earlier—when two developers unknowingly spent days modifying the same piece of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Question Is: Why Did This Happen?
&lt;/h2&gt;

&lt;p&gt;Whenever someone tells me:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Git merge conflicts are driving me crazy."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I usually ask one question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long was your branch open?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;More often than not, the answer is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;four days&lt;/li&gt;
&lt;li&gt;one week&lt;/li&gt;
&lt;li&gt;sometimes even three weeks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's usually where the real problem begins.&lt;/p&gt;

&lt;p&gt;Every day your branch stays isolated, the main branch keeps moving.&lt;/p&gt;

&lt;p&gt;Someone renames classes.&lt;/p&gt;

&lt;p&gt;Someone refactors a service.&lt;/p&gt;

&lt;p&gt;Someone fixes a production bug.&lt;/p&gt;

&lt;p&gt;Someone updates dependencies.&lt;/p&gt;

&lt;p&gt;By the time you're ready to merge, your code is no longer based on today's project—it's based on the project from last week.&lt;/p&gt;

&lt;p&gt;Git is simply the first tool to point that out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost Isn't the Conflict
&lt;/h2&gt;

&lt;p&gt;People often think merge conflicts are expensive because they take time to resolve.&lt;/p&gt;

&lt;p&gt;In reality, editing the conflicting lines usually takes only a few minutes.&lt;/p&gt;

&lt;p&gt;Understanding &lt;strong&gt;why&lt;/strong&gt; the conflict exists is what costs time.&lt;/p&gt;

&lt;p&gt;Imagine this situation.&lt;/p&gt;

&lt;p&gt;You started a feature on Monday.&lt;/p&gt;

&lt;p&gt;By Friday:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;six pull requests have been merged&lt;/li&gt;
&lt;li&gt;your service has been partially refactored&lt;/li&gt;
&lt;li&gt;another developer extracted shared logic into a new class&lt;/li&gt;
&lt;li&gt;someone fixed a production bug in the same module&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now Git reports a conflict.&lt;/p&gt;

&lt;p&gt;You're no longer solving one conflict.&lt;/p&gt;

&lt;p&gt;You're rebuilding your mental model of everything that happened while you were working.&lt;/p&gt;

&lt;p&gt;That's the expensive part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Large Pull Requests Multiply the Problem
&lt;/h2&gt;

&lt;p&gt;One pattern appears almost everywhere.&lt;/p&gt;

&lt;p&gt;Large pull requests almost always create larger merge conflicts.&lt;/p&gt;

&lt;p&gt;It isn't surprising.&lt;/p&gt;

&lt;p&gt;A pull request with 2,000 changed lines touches dozens of files.&lt;/p&gt;

&lt;p&gt;A pull request with 150 lines might touch only two or three.&lt;/p&gt;

&lt;p&gt;Which one is more likely to overlap with someone else's work?&lt;/p&gt;

&lt;p&gt;Exactly.&lt;/p&gt;

&lt;p&gt;This is one of the biggest reasons I prefer small pull requests.&lt;/p&gt;

&lt;p&gt;Smaller changes don't just make reviews easier.&lt;/p&gt;

&lt;p&gt;They dramatically reduce the chance that multiple developers modify the same code at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Long-Lived Branches Drift Away From Reality
&lt;/h2&gt;

&lt;p&gt;Feature branches aren't the problem.&lt;/p&gt;

&lt;p&gt;Long-lived feature branches are.&lt;/p&gt;

&lt;p&gt;I've seen teams spend two weeks building a feature before opening their first pull request.&lt;/p&gt;

&lt;p&gt;By then, the main branch has evolved so much that merging becomes an entire task on its own.&lt;/p&gt;

&lt;p&gt;Developers spend hours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fixing conflicts&lt;/li&gt;
&lt;li&gt;updating failing tests&lt;/li&gt;
&lt;li&gt;adapting to refactored services&lt;/li&gt;
&lt;li&gt;resolving dependency changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ironically, teams often conclude that merging is painful.&lt;/p&gt;

&lt;p&gt;The truth is slightly different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merging became painful because they waited too long.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Communication Prevents More Conflicts Than Git Ever Will
&lt;/h2&gt;

&lt;p&gt;Not every conflict can be avoided.&lt;/p&gt;

&lt;p&gt;Sometimes two developers genuinely need to work in the same part of the application.&lt;/p&gt;

&lt;p&gt;The difference is whether they know about each other.&lt;/p&gt;

&lt;p&gt;Imagine this.&lt;/p&gt;

&lt;p&gt;One developer is refactoring the authentication service.&lt;/p&gt;

&lt;p&gt;Another developer is implementing passwordless login.&lt;/p&gt;

&lt;p&gt;Without communication, both spend several days modifying the same code.&lt;/p&gt;

&lt;p&gt;The conflict is inevitable.&lt;/p&gt;

&lt;p&gt;Now imagine a five-minute conversation before they start.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"I'm working on AuthService this week."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Me too."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Let's split the work and merge more often."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Five minutes of communication can save hours of unnecessary conflict resolution.&lt;/p&gt;

&lt;p&gt;Git could never solve that problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trunk-Based Development Changes Everything
&lt;/h2&gt;

&lt;p&gt;This is one of the biggest reasons I'm such a fan of Trunk-Based Development.&lt;/p&gt;

&lt;p&gt;The goal isn't to eliminate merge conflicts completely.&lt;/p&gt;

&lt;p&gt;That's impossible.&lt;/p&gt;

&lt;p&gt;The goal is to make them so small that nobody really cares.&lt;/p&gt;

&lt;p&gt;Instead of merging once every Friday...&lt;/p&gt;

&lt;p&gt;...developers merge several times every day.&lt;/p&gt;

&lt;p&gt;Instead of resolving fifty conflicts at once...&lt;/p&gt;

&lt;p&gt;...they resolve one tiny overlap before lunch.&lt;/p&gt;

&lt;p&gt;Small branches create small pull requests.&lt;/p&gt;

&lt;p&gt;Small pull requests create small conflicts.&lt;/p&gt;

&lt;p&gt;Small conflicts rarely interrupt development.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Most Dangerous Conflicts Never Appear in Git
&lt;/h2&gt;

&lt;p&gt;Interestingly, the worst conflicts aren't merge conflicts at all.&lt;/p&gt;

&lt;p&gt;Imagine this scenario.&lt;/p&gt;

&lt;p&gt;Developer A changes how discounts are calculated.&lt;/p&gt;

&lt;p&gt;Developer B changes invoice generation.&lt;/p&gt;

&lt;p&gt;Different files.&lt;/p&gt;

&lt;p&gt;No merge conflict.&lt;/p&gt;

&lt;p&gt;Everything merges perfectly.&lt;/p&gt;

&lt;p&gt;Production breaks because invoice generation still expects the previous discount logic.&lt;/p&gt;

&lt;p&gt;Git couldn't detect that.&lt;/p&gt;

&lt;p&gt;The problem wasn't textual.&lt;/p&gt;

&lt;p&gt;It was logical.&lt;/p&gt;

&lt;p&gt;These hidden conflicts become much easier to discover when teams integrate continuously instead of waiting days before merging.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Reduce Merge Conflicts
&lt;/h2&gt;

&lt;p&gt;No workflow eliminates merge conflicts completely.&lt;/p&gt;

&lt;p&gt;But a few simple habits make them significantly less painful.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Merge into the main branch frequently.&lt;/li&gt;
&lt;li&gt;Keep pull requests small.&lt;/li&gt;
&lt;li&gt;Avoid combining unrelated changes.&lt;/li&gt;
&lt;li&gt;Communicate when multiple developers work in the same area.&lt;/li&gt;
&lt;li&gt;Use feature flags instead of long-lived branches.&lt;/li&gt;
&lt;li&gt;Review pull requests quickly so they don't sit idle for days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these practices are revolutionary.&lt;/p&gt;

&lt;p&gt;Together, however, they change merge conflicts from major interruptions into minor inconveniences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop Blaming Git
&lt;/h2&gt;

&lt;p&gt;Git has become one of the easiest tools to blame.&lt;/p&gt;

&lt;p&gt;It reports the conflict.&lt;/p&gt;

&lt;p&gt;It highlights the differences.&lt;/p&gt;

&lt;p&gt;It forces developers to make a decision.&lt;/p&gt;

&lt;p&gt;But Git didn't create the problem.&lt;/p&gt;

&lt;p&gt;It simply revealed it.&lt;/p&gt;

&lt;p&gt;The next time your team spends an afternoon resolving merge conflicts, don't ask:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"How can we get better at Git?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ask something much more valuable:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"What in our development process allowed this conflict to grow this large?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most of the time, the answer isn't hidden in a Git command.&lt;/p&gt;

&lt;p&gt;It's hidden in the workflow.&lt;/p&gt;

&lt;p&gt;And once the workflow improves, merge conflicts become exactly what they should be: small, occasional interruptions instead of recurring team-wide headaches.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>git</category>
      <category>devops</category>
      <category>development</category>
    </item>
    <item>
      <title>The Specification Pattern in Laravel: Refactoring Complex Business Rules</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 14 Jul 2026 14:43:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/the-specification-pattern-in-laravel-refactoring-complex-business-rules-40k2</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/the-specification-pattern-in-laravel-refactoring-complex-business-rules-40k2</guid>
      <description>&lt;p&gt;As our Laravel applications grow, we inevitably face a common architectural dilemma: &lt;strong&gt;where do we put complex business logic that dictates whether an entity meets certain criteria?&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Suddenly, your code becomes littered with long chains of Eloquent scopes in your queries, and worse, identical blocks of if-else statements in your PHP services to check the exact same logic for already-loaded models.&lt;/p&gt;

&lt;p&gt;This is where the codebase starts to smell. We are violating the Single Responsibility Principle, duplicating rules, and creating a maintenance nightmare. To fix this, developers often reach for complex design patterns that end up overengineering the solution, violating the KISS (Keep It Simple, Stupid) principle.&lt;/p&gt;

&lt;p&gt;But there is a elegant, highly practical pattern that solves this perfectly without breaking a sweat: &lt;strong&gt;The Specification Pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous article in refactoring category:&lt;/strong&gt; &lt;a href="https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/06/15/laravel-event-driven-architecture/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real-World Problem: The "Premium Customer" Mess
&lt;/h2&gt;

&lt;p&gt;Let’s look at a concrete example. Imagine an e-commerce platform where we need to determine if a customer qualifies for a VIP Discount.&lt;/p&gt;

&lt;p&gt;The business rules for a VIP customer are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The account must be active.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The user must have a verified email.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They must have made at least 5 orders in total.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They must not be on our fraud blacklist.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Naive Laravel Approach
&lt;/h2&gt;

&lt;p&gt;Typically, you see developers solving this in two ways. First, they write a massive query in a controller or service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In a Controller or Service&lt;/span&gt;
&lt;span class="nv"&gt;$vipUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'is_active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email_verified_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereDoesntHave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'flags'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fraud'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works fine for fetching data from the database. But what happens when you already have a User instance in memory—say, inside a Job or an Event Listener—and you need to check if this specific user qualifies?&lt;/p&gt;

&lt;p&gt;You end up duplicating the logic in pure PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;qualifiesForVipDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt; 
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email_verified_at&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fraud'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this breaks the KISS principle:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Duplication: If the fraud rule changes, you have to find and rewrite both the SQL/Eloquent logic and the in-memory PHP logic.&lt;/li&gt;
&lt;li&gt;Model Bloat: Shoving this into the User model makes it a "God Object" over time.&lt;/li&gt;
&lt;li&gt;Hidden Rules: Your business logic is trapped inside technical database implementation details.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Enter the Specification Pattern
&lt;/h2&gt;

&lt;p&gt;The Specification Pattern solves this by turning a business rule into a first-class citizen—a single, isolated, reusable class.&lt;/p&gt;

&lt;p&gt;A strict specification pattern can sometimes get overly complex with abstract syntax trees and custom expression builders. To keep it &lt;strong&gt;KISS-compliant&lt;/strong&gt;, we are going to build a pragmatic version tailored for modern PHP 8.5+ and Laravel.&lt;/p&gt;

&lt;p&gt;Let's define a simple interface for our specifications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Specifications&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * Check if a given object satisfies the specification in-memory.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;isSatisfiedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Apply the specification directly to an Eloquent query builder.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementing the Specification
&lt;/h2&gt;

&lt;p&gt;Let's build our IsVipCustomer specification. Instead of writing abstract logic, we put our exact four business rules inside this class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Specifications&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IsVipCustomer&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;isSatisfiedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$candidate&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email_verified_at&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fraud'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'is_active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email_verified_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereDoesntHave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'flags'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fraud'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how clean this is. The technical details of how we determine a VIP customer are encapsulated in exactly one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making It Truly Useful: Combining Specifications
&lt;/h2&gt;

&lt;p&gt;Where this pattern completely demolishes standard refactoring approaches is when business requirements change or combine. What if we want to find users who are &lt;strong&gt;VIP Customers&lt;/strong&gt; AND are &lt;strong&gt;Located in the EU&lt;/strong&gt; (for tax or shipping promotions)?&lt;/p&gt;

&lt;p&gt;Instead of writing a third massive scope, we can create a simple composition layer. Let's create an AndSpecification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Specifications&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AndSpecification&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$specifications&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Specification&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$specifications&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;specifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$specifications&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;isSatisfiedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;specifications&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$spec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$spec&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isSatisfiedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;specifications&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$spec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$spec&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Putting It Into Practice (Where It Pays Off)
&lt;/h2&gt;

&lt;p&gt;Let's look at how this simplifies our daily Laravel tasks. We'll look at a Controller (database querying) and a Service/Job (in-memory validation).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario A: Querying the Database&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inside your Admin Dashboard controller, you need to list all VIP customers from the EU to send them a special newsletter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Specifications\AndSpecification&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Specifications\IsVipCustomer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Specifications\IsLocatedInEU&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VipNewsletterController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Define the composite rule&lt;/span&gt;
        &lt;span class="nv"&gt;$vipInEuSpec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AndSpecification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IsVipCustomer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IsLocatedInEU&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Apply it directly to the query&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$vipInEuSpec&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin.vip-newsletter'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Scenario B: In-Memory Domain Validation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now imagine a completely different part of the app. A user clicks "Claim VIP Reward". The user is already logged in, so we already have the $user instance loaded in memory. We don't want to hit the database with complex heavy joins again if we can avoid it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Specifications\IsVipCustomer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RewardController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;IsVipCustomer&lt;/span&gt; &lt;span class="nv"&gt;$vipSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Check the exact same business rule in-memory&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$vipSpec&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isSatisfiedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'You do not qualify for this reward.'&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Process reward...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Reward claimed!'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This is NOT Overengineering
&lt;/h2&gt;

&lt;p&gt;If you look at the code above, a skeptic might say: "Why did we create three classes when a couple of Eloquent scopes could do the trick?"&lt;/p&gt;

&lt;p&gt;Here is why this approach honors the KISS principle when dealing with real complexity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "Single Source of Truth" rule:&lt;/strong&gt; If the marketing team changes the definition of a VIP customer to require 10 orders instead of 5, you change one number in one class. Your controllers, jobs, and models remain completely untouched.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database vs. Memory consistency:&lt;/strong&gt; Standard Eloquent scopes cannot be run on a regular PHP collection or an already-loaded model without triggering fresh SQL queries. The Specification Pattern bridges this gap natively.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Impeccable Testability:&lt;/strong&gt; Testing complex business logic mixed with controllers is painful. Testing a Specification class is a pure unit test. You pass a mock user, assert true or false, and you're done in milliseconds.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_user_with_insufficient_orders_is_not_vip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'is_active'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email_verified_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRelation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 0 orders&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRelation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'flags'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="nv"&gt;$spec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IsVipCustomer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$spec&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isSatisfiedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary: When to Use It
&lt;/h2&gt;

&lt;p&gt;Don't use this pattern for simple things like User::where('role', 'admin'). That would be overengineering.&lt;/p&gt;

&lt;p&gt;Reach for the Specification Pattern when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A business rule is dynamic and changes often based on business requirements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You need to use the exact same rule for both &lt;strong&gt;database filtering and in-memory validation.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You need to combine multiple business rules dynamically depending on the context (e.g., matching different combinations of criteria for user segmentation).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By encapsulating volatile business logic into isolated specifications, you keep your models thin, your controllers clean, and your application incredibly adaptive to future changes. It’s Clean Architecture at its most practical level.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>programming</category>
      <category>refactorit</category>
    </item>
    <item>
      <title>Testing Laravel Events and Listeners: Ensuring Reliable Asynchronous Workflows</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:48:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/testing-laravel-events-and-listeners-ensuring-reliable-asynchronous-workflows-4gi0</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/testing-laravel-events-and-listeners-ensuring-reliable-asynchronous-workflows-4gi0</guid>
      <description>&lt;p&gt;In modern Laravel applications, events and listeners are the glue that holds our complex business logic together. They allow us to decouple our code, keeping controllers thin and services focused. However, as applications scale, this "decoupling" can become a testing nightmare.&lt;/p&gt;

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

&lt;p&gt;In this guide, we’ll move beyond basic Event::fake() assertions and explore how to build a robust testing strategy for event-driven Laravel applications.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/06/22/tdd-in-laravel/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/06/22/tdd-in-laravel/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with "Testing Too Much"
&lt;/h2&gt;

&lt;p&gt;When developers start testing events, the default instinct is to reach for Event::fake(). It’s easy, it’s fast, and it makes the test pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_order_is_placed&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/checkout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertDispatched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderPlaced&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  1. Unit Testing Listeners in Isolation
&lt;/h2&gt;

&lt;p&gt;The best way to ensure reliability is to treat your Listeners like any other service class. A listener should have one job. If it has complex logic, extract it into a dedicated Action or Service class and test that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Listeners/SendOrderConfirmation.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;OrderPlaced&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Don't put business logic here!&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sendConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By keeping the listener thin, your unit test for SendOrderConfirmation becomes trivial. You can mock the Mailer service and simply verify the handle() method is called correctly. This is fast, deterministic, and catches bugs in the communication layer without overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Integration Testing: The "End-to-End" Event Flow
&lt;/h2&gt;

&lt;p&gt;When you want to verify that the OrderPlaced event actually triggers the SendOrderConfirmation listener and writes the correct data to the database, you need an integration test.&lt;/p&gt;

&lt;p&gt;Crucially, do not fake the event here. Instead, allow the event to dispatch and verify the side effects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_order_placed_event_triggers_side_effects&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Arrange: Setup user and cart&lt;/span&gt;
    &lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Act: Trigger the event directly&lt;/span&gt;
    &lt;span class="nc"&gt;OrderPlaced&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Assert: Check side effects, not just the dispatch&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertDatabaseHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'emails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'order_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sent'&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach proves that your Service Provider is correctly registered and that the binding between the Event and Listener is active.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Handling Asynchronous Queues
&lt;/h2&gt;

&lt;p&gt;The biggest trap is the mix of synchronous and asynchronous listeners. If your listener implements ShouldQueue, Event::fake() will prevent the job from ever being pushed to the queue.&lt;/p&gt;

&lt;p&gt;To test queued listeners, use the Bus and Queue facades in tandem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_order_placed_event_queues_notification&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderPlaced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertPushed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SendOrderConfirmation&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pro Tip: If you are testing a complex flow, don't forget to test the failure state. Use Queue::assertPushed to verify that the retry logic or failure handling (like failed() methods) is configured for your mission-critical jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Avoiding "The Silent Failure"
&lt;/h2&gt;

&lt;p&gt;One common mistake is neglecting what happens when a listener fails. If your application relies on a chain of events, you must test the "atomic" nature of the flow.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Testing Checklist for Events
&lt;/h2&gt;

&lt;p&gt;To keep your test suite maintainable as your project grows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Logic Extraction: If a listener is more than 5 lines, move the logic to an Action class.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fake Sparingly: Use Event::fake() only when you specifically want to verify the triggering of an event, not its outcome.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verify Side Effects: Always write at least one integration test that lets the event fire "for real" to ensure the pipeline is wired up correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Queue Awareness: Always distinguish between testing the dispatch and testing the queued job execution.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>php</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Automated Testing Strategies for Trunk-Based Development: How to Build a 10-Minute Pipeline Home Development Workflow &amp; Best Practices</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 30 Jun 2026 14:44:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline-home-1520</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline-home-1520</guid>
      <description>&lt;p&gt;If you’ve been following my recent posts, you already know I am a big fan of Trunk-Based Development (TBD), tiny pull requests, and deploying to production multiple times a day using feature flags. It sounds like an engineering dream: no long-lived branches, no merge hell on Friday afternoons, and features reaching users in hours instead of months.&lt;/p&gt;

&lt;p&gt;But let’s be honest. When you tell a traditional QA manager or a risk-averse lead that developers are going to merge straight into the main branch and deploy to production ten times a day, they usually have a mini panic attack.&lt;/p&gt;

&lt;p&gt;And they aren’t entirely wrong to worry.&lt;/p&gt;

&lt;p&gt;If your team is merging code directly into the trunk every few hours, you cannot rely on manual regression testing. You cannot wait for a weekly QA cycle, and you certainly cannot spend two hours manually clicking through a staging environment to make sure a bugfix didn’t break the payment gateway.&lt;/p&gt;

&lt;p&gt;To make Trunk-Based Development work without crashing production, you need an automated testing strategy built for speed and high confidence. You need a pipeline that gives you a definitive “Yes” or “No” in under 10 minutes.&lt;/p&gt;

&lt;p&gt;Here is how we can build it from a backend perspective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Traditional Testing Pyramid Fails in TBD
&lt;/h2&gt;

&lt;p&gt;We’ve all seen the classic testing pyramid: a massive base of unit test suites, a middle layer of integration tests, and a tiny peak of End-to-End (E2E) UI tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7l6uoj1ouk9r47sv7v3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7l6uoj1ouk9r47sv7v3g.png" alt=" " width="746" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In theory, it’s a great model. In a Trunk-Based Development workflow, however, teams often warp this pyramid into something that looks more like an ice cream cone. Frontend teams or QA engineers might want to cover every single user flow with massive E2E integration suites to feel safe.&lt;/p&gt;

&lt;p&gt;But as backend developers, we hit a major bottleneck here: heavy end-to-end tests are notoriously slow.&lt;/p&gt;

&lt;p&gt;If your CI pipeline takes 45 minutes to run because it needs to spin up frontend clients, log in dummy users, and click through a shopping cart just to verify a backend API change, your TBD flow breaks down entirely. Developers will stop merging frequently. They will accumulate code locally because waiting for the pipeline feels like a punishment. Suddenly, your small PRs become massive again, and you’re right back to square one.&lt;/p&gt;

&lt;p&gt;To make TBD work, your pipeline must act as a fast feedback loop, not a bureaucratic checkpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Strategy: The 10-Minute Rule
&lt;/h2&gt;

&lt;p&gt;The golden rule of Continuous Integration in a fast-paced team is simple: The commit-to-feedback loop must take less than 10 minutes.&lt;/p&gt;

&lt;p&gt;If a developer pushes a change to the trunk (or opens a tiny, short-lived PR that will be merged within an hour), they should know almost immediately if they broke the build. If it takes longer than 10 minutes, the developer context-switches, starts a new task, and by the time the build fails, they have to drop what they are doing to fix a problem they’ve already forgotten about.&lt;/p&gt;

&lt;p&gt;To achieve a 10-minute pipeline, you have to split your testing strategy into two distinct phases: Pre-Merge (Gatekeeping) and Post-Merge (Asynchronous Verification).&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Pre-Merge Validation (The Gatekeeper)
&lt;/h2&gt;

&lt;p&gt;Before code touches the main branch, your CI pipeline needs to run a highly optimized, strictly isolated suite of checks. This is your first line of defense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Advanced Unit Testing &amp;amp; Mocking&lt;/strong&gt;&lt;br&gt;
Unit tests must form the absolute majority of your test suite. They should test pure logic, edge cases, and data transformations without touching external networks, file systems, or databases.&lt;/p&gt;

&lt;p&gt;Instead of hitting a real database, use clean interfaces and mocks. For example, if you are testing a backend service that calculates a user’s premium subscription discount, do not query the database to fetch the user profile. Pass a mock object or a stub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example of a fast, isolated unit test&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DiscountService&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should apply a 20% discount for premium users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PREMIUM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DiscountService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalPrice&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Runs in milliseconds&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A suite of 2,000 unit tests like this should run in under 60 seconds. If it takes longer, you are likely doing hidden I/O operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Contract Testing over Heavy Integration
&lt;/h2&gt;

&lt;p&gt;When working with backend microservices, a common fear is that changing an API endpoint in Service A will break Service B. The instinctual response is to run massive integration tests that boot up multiple services at once.&lt;/p&gt;

&lt;p&gt;Don’t do this in your pre-merge pipeline. Use Contract Testing instead (using tools like Pact).&lt;/p&gt;

&lt;p&gt;Contract testing allows Service B (the consumer) to define a JSON file stating exactly what format it expects from Service A’s API. During Service A’s pipeline, it simply validates its code against this static contract file. It takes seconds, yet gives you the exact same confidence as a full integration test without spinning up a single extra server or relying on frontend E2E setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Post-Merge Validation (The Safety Net)
&lt;/h2&gt;

&lt;p&gt;Once the code passes the 10-minute gatekeeper, it is merged into the trunk. Now, you can trigger the heavier, slower tests asynchronously. This runs in parallel or immediately after deployment to a staging environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Ephemeral Integration Testing&lt;/strong&gt;&lt;br&gt;
You do need to test real database interactions, but you should do it smart. Use short-lived, isolated environments. Tools like Docker and Testcontainers allow your pipeline to spin up a real PostgreSQL or Redis instance in a container, run database-dependent tests, and tear it down immediately.&lt;/p&gt;

&lt;p&gt;Keep these tests focused purely on persistence logic—like verifying complex SQL queries or transaction rollbacks—rather than business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Handling the “Flaky Test” Epidemic&lt;/strong&gt;&lt;br&gt;
Nothing destroys trust in an automated testing pipeline faster than a flaky test—a test that passes 9 times but fails on the 10th for no apparent reason (usually due to network timing, race conditions, or shared state in integration environments).&lt;/p&gt;

&lt;p&gt;If a developer sees a pipeline fail, checks it, realizes it’s just “that one annoying flaky test again,” and hits Re-run, your testing culture is dying. Developers will start ignoring real failures.&lt;/p&gt;

&lt;p&gt;You must enforce a strict Zero Tolerance Policy for Flakiness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Quarantine immediately: The moment a test shows flaky behavior, move it out of the main pipeline into a separate, non-blocking quarantine suite.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix or delete: If a quarantined test isn’t fixed within a sprint, delete it. A non-existent test is better than a test that lies to your team.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Continuous Verification: Testing in Production
&lt;/h2&gt;

&lt;p&gt;Even with a perfect pipeline, things will eventually go wrong. The ultimate evolution of a Trunk-Based Development testing strategy is recognizing that testing doesn’t stop when you deploy.&lt;/p&gt;

&lt;p&gt;Since we use Feature Flags (as discussed in my last post), we can merge code to production completely hidden from users. This unlocks a powerful technique: Canary Deployments.&lt;/p&gt;

&lt;p&gt;Instead of releasing a change to 100% of your traffic, route just 1% of real users to the new code. Your automated testing strategy here shifts from running test code to monitoring business metrics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmtu4sz638bmbpvsialgp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmtu4sz638bmbpvsialgp.png" alt=" " width="748" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your APM tool (like Datadog or New Relic) detects a sudden spike in HTTP 500 errors or a drop in checkout conversions for that 1% segment, the feature flag automatically flips off.&lt;/p&gt;

&lt;p&gt;Production monitoring is the final, most reliable tier of your testing pyramid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Speed Breeds Quality
&lt;/h2&gt;

&lt;p&gt;Moving to Trunk-Based Development requires a massive shift in how you think about quality assurance. It requires moving away from heavy, slow, protective gates toward fast, automated, and resilient feedback loops.&lt;/p&gt;

&lt;p&gt;If you focus on keeping your unit tests pure, replacing heavy integration tests with contract testing, ruthlessly quarantining flaky tests, and using production monitoring as your final safety net, you can easily hit that 10-minute pipeline goal.&lt;/p&gt;

&lt;p&gt;When your pipeline is that fast, developers catch bugs while the code is still fresh in their minds. It makes development predictable, safe, and incredibly fast.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ci</category>
      <category>devops</category>
      <category>software</category>
    </item>
    <item>
      <title>Test-Driven Development in Laravel: The Red-Green-Refactor Loop</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 23 Jun 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/test-driven-development-in-laravel-the-red-green-refactor-loop-12e5</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/test-driven-development-in-laravel-the-red-green-refactor-loop-12e5</guid>
      <description>&lt;p&gt;We’ve all been there. You have a new feature to build. You open your IDE, create a Controller, start wiring up the logic, maybe add some validation, and then, hours later, you finally write a test to make sure it actually works. Or, if we’re being honest, maybe you don’t write the test at all.&lt;/p&gt;

&lt;p&gt;This is the “code first, ask questions later” approach. And while it feels fast at the start, it’s exactly how we end up with legacy code that nobody dares to touch. It’s how “it works on my machine” turns into a 3:00 AM emergency production fix.&lt;/p&gt;

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

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/06/01/flaky-tests-in-laravel/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cycle Explained: It’s a Rhythm, Not a Rule
&lt;/h2&gt;

&lt;p&gt;The TDD process is a rhythmic loop that keeps you focused.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Red: You write a test for a tiny, specific piece of functionality that doesn’t exist yet. You run it. It should fail. If it passes immediately, your test is either broken or testing nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Green: You write the absolute minimum amount of code to make that test pass. Not perfect code, not optimized code—just enough code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Refactor: Now that you have a safety net, you clean up. You improve naming, extract methods, remove duplication, and optimize. Because you have the test, you can change the internals without breaking the contract.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The “Why”: Beyond Just Catching Bugs
&lt;/h2&gt;

&lt;p&gt;Why go through the trouble? TDD is not just about catching bugs. It’s about &lt;strong&gt;design&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you write the test first, you are forced to step into the shoes of the “consumer” of your code. You stop thinking about how a database table is structured and start thinking about how a controller or a service needs to interact with your business logic. If you find it hard to write a test, you’ve just received the most valuable feedback possible: your design is too complex or your coupling is too tight.&lt;/p&gt;

&lt;p&gt;TDD acts as an early warning system for bad architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real-World Example: Building a “Subscription System”
&lt;/h2&gt;

&lt;p&gt;Let’s say we are building a subscription feature. We need to ensure that when a user subscribes, they receive a welcome email, and their status is updated in the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: The Red Phase (The Requirement)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We start with a feature test. We are defining the behavior, not the implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_user_can_subscribe_to_a_plan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/subscribe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'plan_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'premium_monthly'&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertDatabaseHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'subscriptions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'plan'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'premium_monthly'&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this results in a 404 or an error, because the route doesn’t exist. &lt;strong&gt;Red&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: The Green Phase (The Implementation)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, we write just enough code to make this pass. We might put all the logic in the controller (yes, even if it’s dirty, because that’s the “Green” phase).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Subscription&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'plan'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plan_id&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test passes. &lt;strong&gt;Green&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: The Refactor Phase (The Cleanup)&lt;/strong&gt;&lt;br&gt;
Now that the test is green, we look at the controller. It’s getting a bit crowded. We decide to move the logic into a &lt;em&gt;SubscriptionService&lt;/em&gt;. We move the logic, run the test again—it still passes! We have total confidence that the refactoring didn’t break the core business requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep Dive: Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Over-Mocking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Developers often mock every single dependency, resulting in tests that pass even when the real system is broken. If you are mocking your entire application to test one controller, you aren’t testing reality. Use real database connections (via RefreshDatabase) and real events whenever possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Testing Implementation vs. Behavior&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don’t write tests that check if a private method was called or if a specific variable was set. Test the result. Did the user get the email? Did the database change? Does the UI show the right message? If you test the implementation, your tests will break every time you refactor, which defeats the whole purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Avoiding the “Green” Trap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes, you write a test that is too big. You try to test the entire checkout flow in one test. When it fails, you don’t know why. Break it down. Test the validation of the payment, then the user status, then the email trigger. Small, focused tests are the secret to a high-speed suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Slow Down to Speed Up” Philosophy
&lt;/h2&gt;

&lt;p&gt;The biggest pushback I hear is: “&lt;em&gt;TDD takes twice as long.&lt;/em&gt;”&lt;/p&gt;

&lt;p&gt;It feels like that at first. You are typing more code. You are thinking more. But think about the time you spend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual debugging sessions in the browser.&lt;/li&gt;
&lt;li&gt;Writing “temporary” logging statements to trace data flow.&lt;/li&gt;
&lt;li&gt;Fear of deploying because you don’t know what might break.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With TDD, that “debugging” time disappears. You catch the logic error within three seconds of writing it. That is where the speed comes from. TDD isn’t about writing code faster; it’s about &lt;strong&gt;shipping with confidence&lt;/strong&gt; because your test suite is your documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts for 2026
&lt;/h2&gt;

&lt;p&gt;Modern Laravel development is about clean, maintainable logic. If you want to survive a project that grows over several years, you cannot rely on memory or manual testing.&lt;/p&gt;

&lt;p&gt;TDD isn’t a religion; it’s a discipline. Start today. Pick one small class or service. Write the test, watch it fail, and then write the code to fix it. Over time, it won’t feel like “work”—it will feel like having a second pair of eyes constantly reviewing your code as you type.&lt;/p&gt;

&lt;p&gt;Happy coding.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>laravel</category>
      <category>php</category>
      <category>programming</category>
    </item>
    <item>
      <title>Beyond Fat Controllers: Mastering Event-Driven Decoupling in Laravel</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Thu, 18 Jun 2026 18:07:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/beyond-fat-controllers-mastering-event-driven-decoupling-in-laravel-2dd6</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/beyond-fat-controllers-mastering-event-driven-decoupling-in-laravel-2dd6</guid>
      <description>&lt;p&gt;As your Laravel application scales, your controllers often evolve into a "dumping ground" for business logic. You start with a straightforward registration flow, and before you know it, you are juggling email notifications, Slack alerts, audit logging, and third-party API calls—all packed into a single method.&lt;/p&gt;

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

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/05/25/state-pattern-vs-enums-in-modern-php/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost of Tight Coupling
&lt;/h2&gt;

&lt;p&gt;Consider a typical registration process in a Laravel application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;RegisterRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// Tightly coupled dependencies&lt;/span&gt;
    &lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'User registered: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Analytics&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_signup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nc"&gt;Newsletter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Success'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is intuitive, yet architecturally toxic. The controller is burdened with infrastructure knowledge. If your Newsletter API slows down, your user registration experiences lag. If the Mail service throws an exception, the entire request fails, potentially causing data incons&lt;/p&gt;

&lt;h2&gt;
  
  
  The KISS Philosophy: Events and Listeners
&lt;/h2&gt;

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

&lt;p&gt;Events act as an intermediary layer. Your controller simply broadcasts the fact that an action occurred, and various listeners react independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Defining the Event&lt;/strong&gt;&lt;br&gt;
Think of an event as a "data transfer object" that carries necessary context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRegistered&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. The Lean Controller&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, look at how the controller looks after refactoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;RegisterRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// Announce the action&lt;/span&gt;
    &lt;span class="nc"&gt;UserRegistered&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Success'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By decoupling, the controller is now focused solely on persistence and orchestration, not on the side effects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Implementing Asynchronous Listeners&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The true power of events in Laravel emerges when you utilize background processing. By implementing the ShouldQueue interface, you move the heavy lifting away from the HTTP request cycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UserRegistered&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Architecture Scales
&lt;/h2&gt;

&lt;p&gt;When you embrace events, you gain more than just cleaner controllers; you gain system resilience:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Fault Tolerance: If your Newsletter service is temporarily unavailable, the listener can automatically retry the task without affecting the user's registration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Performance: The user receives a 201 response immediately, while secondary tasks (like analytics) run in the background.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Extensibility: Need to add a "Push Notification" when a user registers? Just create a new listener. You don’t need to touch the registration controller logic at all, which eliminates the risk of regression bugs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Testing Contracts: You can now test the registration process by asserting that the UserRegistered event was fired, without needing to mock complex external mail or log services.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Sandbox Example
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Theory is one thing, but code in action speaks louder. [Here you can see a live example] of what EventDispatcher looks like in an isolated environment and try out how easy it is to add a new listener without changing the core logic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try this sandbox example by yourself:&lt;/strong&gt; &lt;a href="https://onlinephp.io/c/1f7b2" rel="noopener noreferrer"&gt;https://onlinephp.io/c/1f7b2&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Complexity: The "Event Hell" Warning
&lt;/h2&gt;

&lt;p&gt;While events are powerful, they are not a silver bullet. An excess of events—or "Event Hell"—can lead to a "spaghetti" flow where the execution path is impossible to track.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow these best practices to maintain sanity:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Don't Over-Abstract: If a piece of code is used only in one place and will never change, don't create an event. A simple function call is more readable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Focus on Side Effects: Events are for post-processing. Do not use events for core logic that must happen synchronously for the application to function.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Descriptive Naming: Use past-tense names (OrderPlaced, InvoiceGenerated). This clearly signifies that the action has already been successfully committed to the database.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deep Dive: Events vs. Model Observers
&lt;/h2&gt;

&lt;p&gt;A frequent question is: "When should I use Eloquent Observers instead of Events?"&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Handling Failures in Background Jobs
&lt;/h2&gt;

&lt;p&gt;When you move to ShouldQueue, you must plan for failure. In Laravel, you can define how your listeners handle retries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Wait 60 seconds between retries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that temporary outages (like a flickering API connection) don't result in lost data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

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

</description>
      <category>programming</category>
      <category>laravel</category>
      <category>php</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>How to Deploy 10 Times a Day Safely with Feature Flags</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 09 Jun 2026 21:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/how-to-deploy-10-times-a-day-safely-with-feature-flags-3m92</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/how-to-deploy-10-times-a-day-safely-with-feature-flags-3m92</guid>
      <description>&lt;p&gt;If you’ve been following my previous posts, you know I’m a big advocate for Trunk-Based Development and shrinking your pull requests until they almost feel too small. In a perfect world, developers merge code directly into the main branch multiple times a day, everything flows smoothly, and production remains rock solid.&lt;/p&gt;

&lt;p&gt;But let’s be honest. When you actually try to pitch this to a backend team working on a core system, you almost always hit the exact same wall of resistance.&lt;/p&gt;

&lt;p&gt;Someone in the back of the room will inevitably raise their hand and ask: &lt;em&gt;“That sounds great in theory, but I’m currently refactoring our legacy checkout service. It’s going to take me four days of deep architectural changes. Are you seriously telling me I should merge half-baked, broken code into the main trunk and push it straight to production where real customers are buying our products?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It’s a completely valid objection. If your only tool for hiding uncompleted work is holding onto a massive, long-lived feature branch, then trunk-based development breaks down immediately. You end up with the exact nightmare we talked about earlier: huge code reviews, painful merge conflicts, and code that rots before it ever sees a live environment.&lt;/p&gt;

&lt;p&gt;To make continuous delivery actually work without causing catastrophic production outages every single afternoon, you need to decouple two concepts that most engineering teams mistakenly treat as the exact same thing: &lt;strong&gt;Deployment&lt;/strong&gt; and &lt;strong&gt;Release&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Last article in this category is focused on Trunk-Based Development: &lt;a href="https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Concept: Shifting Left by Decoupling
&lt;/h2&gt;

&lt;p&gt;In traditional development setups, deploying code and releasing a feature happen simultaneously. You merge your giant feature branch, the CI/CD pipeline runs, the code hits the live servers, and boom—your users immediately see the new functionality.&lt;/p&gt;

&lt;p&gt;This model is incredibly high-stakes. If something goes wrong, your only options are rolling back the entire deployment (which might contain unrelated fixes from other developers) or rushing a frantic hotfix through the pipeline while customer support tickets pile up and management starts breathing down your neck.&lt;/p&gt;

&lt;p&gt;Feature flags (or feature toggles) completely change this dynamic by shifting the risk layout.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment means moving bits to servers. Your code lives in the production environment, executing safely under the hood, but it remains invisible or inaccessible to the end user. It’s a technical activity.&lt;/li&gt;
&lt;li&gt;Release means making that code active for users. It’s a business decision, completely independent of the deployment schedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By wrapping your new code inside a simple conditional statement, you can safely deploy unfinished logic to production ten times a day. The code is physically there on your production servers, but the execution path is dormant. You’ve successfully removed the stress from the deployment process.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Realistic Look at the Code
&lt;/h2&gt;

&lt;p&gt;Let’s skip the over-engineered enterprise frameworks for a moment and look at how this actually plays out in a standard backend context. Imagine you are upgrading a legacy payment gateway integration to a new, more reliable third-party API provider.&lt;/p&gt;

&lt;p&gt;Instead of waiting weeks to swap the entire implementation out in one massive, terrifying PR, you introduce a flag. In its simplest form, it looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentProcessor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;NewPaymentGateway&lt;/span&gt; &lt;span class="n"&gt;newGateway&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;LegacyPaymentGateway&lt;/span&gt; &lt;span class="n"&gt;legacyGateway&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;FeatureFlagClient&lt;/span&gt; &lt;span class="n"&gt;flagClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flagClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"use-new-payment-gateway"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUserId&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;newGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;legacyGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Fallback safety net&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flagClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"use-new-payment-gateway"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUserId&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"New gateway failed, falling back to legacy for user: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUserId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;legacyGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we aren’t just checking a global &lt;em&gt;true/false&lt;/em&gt; boolean config value. We are passing the &lt;em&gt;order.getUserId()&lt;/em&gt; into the flag client. This allows for runtime evaluation based on context.&lt;/p&gt;

&lt;p&gt;With this setup, you can merge your new gateway code when it’s only 20% finished. The interface is there, the basic structure is set, but the flag is turned off for everyone in production. You get to test your integration continuously against real staging environments or hidden production paths without risking a single actual customer transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Database Problem: Handling Migrations Safely
&lt;/h2&gt;

&lt;p&gt;One common argument against frequent deployments with feature flags is: “What about database changes? You can’t just feature-flag a schema migration.” This is where many teams stumble. If your code depends on a new database column that doesn’t exist yet, your application will crash. To solve this, your database strategy must evolve alongside your code isolation. You have to follow the &lt;strong&gt;Expand and Contract&lt;/strong&gt; pattern.&lt;/p&gt;

&lt;p&gt;Instead of renaming or modifying a column in a single destructive step, you break the change down into multiple backward-compatible deployments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Expand: You deploy a migration that adds the new column or table. The old code doesn’t know it exists, so nothing breaks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dual Write: You deploy a feature flag that starts writing data to both the old and new columns simultaneously, but still reads only from the old one. This ensures your new schema populates with live data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Backfill: You run a background script to copy historical data from the old structure to the new one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flip the Switch: You change the feature flag to read from the new column. If performance degrades, you slide the flag back instantly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Contract: Once you are 100% confident, you remove the feature flag, delete the old code path, and deploy a final migration to drop the old column.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Yes, it requires more steps. But it transforms a terrifying database migration into a series of boring, completely safe tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving Beyond Simple Booleans: Dark Launching and Canaries
&lt;/h2&gt;

&lt;p&gt;Once you separate deployment from release, you unlock deployment workflows that make standard staging environments look completely obsolete. The most powerful of these is the &lt;strong&gt;Canary Release&lt;/strong&gt; (or gradual rollout).&lt;/p&gt;

&lt;p&gt;Instead of flipping a switch and hoping your database doesn’t melt under a new query load, you can configure your feature flag system to evaluate based on percentages or specific user attributes. A realistic rollout plan for our new payment gateway looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Internal Testing (The QA Tier)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The flag is enabled only for internal QA team user IDs or specifically whitelisted corporate IP addresses. You are running tests on the live production infrastructure, using real database connections, but nobody outside your company knows about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: The Canary (1% Traffic)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You route exactly 1% of random global traffic through the new gateway. You sit back and monitor your logging dashboard for an hour. You look for spikes in 500 errors, increased latency, or unusual database connection pool exhaustion. If 1% of your users experience a bug, it’s a minor issue you can catch quickly, rather than a company-wide outage affecting everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: The Ramp-Up (10% -&amp;gt; 50%)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the metrics look clean after 24 hours, you scale the flag to 10%, then 50% over the next two days. This gradual increase helps you see how the system behaves under a realistic load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Full Release (100%)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The feature is stable, metrics are perfect, and the old legacy gateway is officially ready for decommissioning.&lt;/p&gt;

&lt;p&gt;If a subtle edge-case bug appears when you hit the 10% mark, you don’t panic. You don’t trigger a full rollback of the service container, which might take 15 minutes to compile and deploy. You simply log into your feature flag dashboard, slide the toggle back to 0%, and fix the bug at your own pace during normal working hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dark Side: Managing the Architecture Debt
&lt;/h2&gt;

&lt;p&gt;If you talk to any backend engineer who has used feature flags in a messy, fast-moving project, they will warn you about the exact same thing: technical debt. It is incredibly easy to treat feature flags like a magic wand, scattering them everywhere until your codebase looks like a tangled bowl of conditional spaghetti.&lt;/p&gt;

&lt;p&gt;If a flag stays in your code for six months after a feature has rolled out to 100% of users, it stops being a tool for continuous delivery and becomes an architectural liability. It makes the code harder to read, complicates unit testing because you have to mock multiple flag states, and leaves dead code paths hanging around indefinitely.&lt;/p&gt;

&lt;p&gt;To prevent your system from turning into an unmaintainable maze, you need to establish strict engineering discipline around the lifecycle of a flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat Toggles as Temporary Scaffolding
&lt;/h2&gt;

&lt;p&gt;Every time you create a feature flag, you should immediately create a corresponding ticket in your backlog to remove that flag. The definition of done for a new feature shouldn’t just be “it works in production.” It must be “it works in production, the old legacy code is deleted, and the flag conditional is completely stripped out of the codebase.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Flag Owner Assignment
&lt;/h2&gt;

&lt;p&gt;Every flag must have a clear owner—either a specific developer or a product team. If a flag sits unchanged for more than four weeks, the automated system or team lead should trigger a warning to review its status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Them Short-Lived
&lt;/h2&gt;

&lt;p&gt;Release toggles should rarely live longer than a single development sprint or two. If a flag has been at 100% for more than a few days without complaints, it’s time to schedule a quick cleanup PR. Don’t let them turn into permanent configuration settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Your Tools Safely
&lt;/h2&gt;

&lt;p&gt;You don’t need to build a massive, complex internal configuration platform from scratch to get started. For smaller teams, a simple, centralized database table or a Redis-backed configuration file that reloads dynamically can be enough to get your feet wet.&lt;/p&gt;

&lt;p&gt;As your team expands and you need advanced targeting rules, percentage rollouts, and audit logs, looking at dedicated tools like LaunchDarkly, Flagsmith, or open-source solutions like Unleash becomes highly valuable.&lt;/p&gt;

&lt;p&gt;The critical architectural requirement is that evaluating a flag must be lightning-fast. It cannot introduce a blocking HTTP request into your critical backend path every time a function is called; it needs to resolve locally in memory via cached flag states that sync asynchronously in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Transitioning to a workflow where you deploy to production ten times a day isn’t an engineering flex—it’s about reducing anxiety. It changes the entire culture of a development team. Production deployments stop being high-stress, late-night events that require everyone to be on standby with their laptops open. They become boring, routine non-events that happen continuously in the background while you grab a coffee or focus on your next task.&lt;/p&gt;

&lt;p&gt;Feature flags are the missing link that makes this possible. They give you the safety net to keep your pull requests tiny, your main branch green, and your delivery pipeline moving forward without ever breaking the user experience.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>development</category>
      <category>software</category>
      <category>devops</category>
    </item>
    <item>
      <title>Flaky Tests in Laravel: Why Your CI Randomly Fails</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Sun, 07 Jun 2026 17:37:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/flaky-tests-in-laravel-why-your-ci-randomly-fails-3m8n</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/flaky-tests-in-laravel-why-your-ci-randomly-fails-3m8n</guid>
      <description>&lt;p&gt;Your test suite passes locally.&lt;br&gt;
CI fails.&lt;/p&gt;

&lt;p&gt;You rerun the pipeline.&lt;br&gt;
Now everything is green.&lt;/p&gt;

&lt;p&gt;You change absolutely nothing.&lt;br&gt;
An hour later, another random failure appears.&lt;/p&gt;

&lt;p&gt;If this sounds familiar, you are probably dealing with flaky tests.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;And once developers stop trusting tests, they start ignoring failures, rerunning pipelines blindly, and eventually shipping bugs to production.&lt;/p&gt;

&lt;p&gt;After dealing with flaky tests in multiple Laravel projects, I noticed something important:&lt;/p&gt;

&lt;p&gt;Most flaky tests are not caused by PHPUnit itself.&lt;/p&gt;

&lt;p&gt;They are usually caused by hidden shared state, timing assumptions, asynchronous behavior, or infrastructure leaking between tests.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show the most common causes of flaky tests in Laravel and how to fix them properly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous article in Testing category:&lt;/strong&gt; &lt;a href="https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/05/09/how-mutation-testing-exposes-the-truth-php-2026-edition/&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What Makes a Test “Flaky”?
&lt;/h2&gt;

&lt;p&gt;A flaky test has three characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It fails inconsistently&lt;/li&gt;
&lt;li&gt;The failure is difficult to reproduce&lt;/li&gt;
&lt;li&gt;Rerunning the test often “fixes” it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is different from a normal failing test.&lt;/p&gt;

&lt;p&gt;A normal failing test indicates a deterministic bug.&lt;/p&gt;

&lt;p&gt;A flaky test creates uncertainty.&lt;/p&gt;

&lt;p&gt;And uncertainty is dangerous in CI pipelines because developers eventually stop taking failures seriously.&lt;/p&gt;


&lt;h1&gt;
  
  
  1. Time-Dependent Tests
&lt;/h1&gt;

&lt;p&gt;One of the most common sources of flaky tests is time.&lt;/p&gt;

&lt;p&gt;Laravel makes working with time easy through Carbon, but time-based logic can easily become unstable.&lt;/p&gt;

&lt;p&gt;Consider this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_subscription_expires_after_24_hours&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Subscription&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'expires_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addDay&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$subscription&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isExpired&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test may pass most of the time.&lt;/p&gt;

&lt;p&gt;But depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI speed&lt;/li&gt;
&lt;li&gt;server load&lt;/li&gt;
&lt;li&gt;execution timing&lt;/li&gt;
&lt;li&gt;timezone handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;it can eventually fail unpredictably.&lt;/p&gt;

&lt;p&gt;The fix is simple:&lt;/p&gt;

&lt;p&gt;Use fixed time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;setTestNow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-05-28 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Subscription&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'expires_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addDay&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$subscription&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isExpired&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And always clean up afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;setTestNow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without cleanup, fake time can leak into other tests and create even more randomness.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Shared Database State
&lt;/h1&gt;

&lt;p&gt;Another massive source of flaky tests is database leakage between tests.&lt;/p&gt;

&lt;p&gt;I still see projects where tests depend on records created by previous tests.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_user_can_create_post&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertDatabaseCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, this looks harmless. However, once another test inserts posts into the database, the count may suddenly become 2, 5, or even 12.&lt;/p&gt;

&lt;p&gt;The fix is proper database isolation.&lt;/p&gt;

&lt;p&gt;In Laravel, this usually means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;RefreshDatabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;DatabaseTransactions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;depending on your architecture.&lt;/p&gt;

&lt;p&gt;I already wrote an entire article comparing these approaches because using the wrong one can create hidden instability.&lt;/p&gt;

&lt;p&gt;The important part is this:&lt;/p&gt;

&lt;p&gt;Tests should never depend on leftovers from previous tests.&lt;/p&gt;

&lt;p&gt;Ever.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Random Factories
&lt;/h1&gt;

&lt;p&gt;Factories are great.&lt;/p&gt;

&lt;p&gt;Randomness is not.&lt;/p&gt;

&lt;p&gt;This test looks innocent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if the factory generates random roles, this test becomes unstable immediately.&lt;/p&gt;

&lt;p&gt;I’ve seen this problem especially in large Laravel projects where factories evolved over years and slowly accumulated randomness everywhere.&lt;/p&gt;

&lt;p&gt;Instead, explicitly define required state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'role'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deterministic data creates deterministic tests.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Queue and Async Problems
&lt;/h1&gt;

&lt;p&gt;Queues are one of the biggest sources of flaky behavior.&lt;/p&gt;

&lt;p&gt;Especially when developers partially fake queues while still allowing some jobs to execute asynchronously.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SendInvoiceJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertDatabaseHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoices'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can fail because the queued job never actually runs.&lt;/p&gt;

&lt;p&gt;Or worse:&lt;br&gt;
it runs sometimes depending on environment configuration.&lt;/p&gt;

&lt;p&gt;Another common issue is testing behavior immediately after dispatching async jobs.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SyncProductsJob&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertDatabaseCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assertion may execute before the worker finishes.&lt;/p&gt;

&lt;p&gt;Locally it passes.&lt;/p&gt;

&lt;p&gt;In CI it randomly fails.&lt;/p&gt;

&lt;p&gt;A better approach is either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;testing the dispatch itself,&lt;/li&gt;
&lt;li&gt;or running jobs synchronously during tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Bus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SyncProductsJob&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nc"&gt;Bus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertDispatched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SyncProductsJob&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'queue.default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sync'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;during the test environment.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Parallel Testing Issues
&lt;/h1&gt;

&lt;p&gt;Parallel testing speeds up CI dramatically.&lt;/p&gt;

&lt;p&gt;But it also exposes hidden shared state.&lt;/p&gt;

&lt;p&gt;I’ve seen failures caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shared Redis keys&lt;/li&gt;
&lt;li&gt;shared files&lt;/li&gt;
&lt;li&gt;cached config&lt;/li&gt;
&lt;li&gt;temporary directories&lt;/li&gt;
&lt;li&gt;static variables&lt;/li&gt;
&lt;li&gt;singleton state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;disk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'report.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If multiple tests write the same file simultaneously, random failures appear.&lt;/p&gt;

&lt;p&gt;The fix is isolation.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or unique filenames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.pdf'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parallel testing does not create flaky tests.&lt;/p&gt;

&lt;p&gt;It reveals problems that already existed.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. External APIs
&lt;/h1&gt;

&lt;p&gt;Real HTTP calls inside tests are dangerous.&lt;/p&gt;

&lt;p&gt;Sometimes the API is slow.&lt;/p&gt;

&lt;p&gt;Sometimes rate limits trigger.&lt;/p&gt;

&lt;p&gt;Sometimes sandbox environments fail.&lt;/p&gt;

&lt;p&gt;And suddenly your test suite becomes unreliable for reasons completely outside your application.&lt;/p&gt;

&lt;p&gt;This is why external APIs should usually be mocked or faked.&lt;/p&gt;

&lt;p&gt;Laravel provides excellent HTTP faking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'*'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your tests become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;faster&lt;/li&gt;
&lt;li&gt;deterministic&lt;/li&gt;
&lt;li&gt;independent from network stability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. Tests That Depend on Execution Order
&lt;/h1&gt;

&lt;p&gt;This one is extremely dangerous.&lt;/p&gt;

&lt;p&gt;A test passes only because another test ran before it.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_admin_exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertDatabaseHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'admin@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This silently depends on another test creating the admin user first.&lt;/p&gt;

&lt;p&gt;Run tests individually and this suddenly fails.&lt;/p&gt;

&lt;p&gt;A good test should work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;independently&lt;/li&gt;
&lt;li&gt;repeatedly&lt;/li&gt;
&lt;li&gt;in any order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If execution order matters, the suite is fragile.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Flaky Tests Become Expensive
&lt;/h1&gt;

&lt;p&gt;The biggest problem with flaky tests is not technical.&lt;/p&gt;

&lt;p&gt;It is psychological.&lt;/p&gt;

&lt;p&gt;Once developers stop trusting CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;failures get ignored&lt;/li&gt;
&lt;li&gt;reruns become normal&lt;/li&gt;
&lt;li&gt;real bugs get missed&lt;/li&gt;
&lt;li&gt;confidence disappears&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve seen teams where developers reran pipelines three or four times automatically because “CI is always flaky anyway.”&lt;/p&gt;

&lt;p&gt;That is dangerous.&lt;/p&gt;

&lt;p&gt;Because eventually a real regression hides inside the noise.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Flaky tests are rarely random.&lt;/p&gt;

&lt;p&gt;There is almost always an underlying engineering problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shared state&lt;/li&gt;
&lt;li&gt;uncontrolled time&lt;/li&gt;
&lt;li&gt;async behavior&lt;/li&gt;
&lt;li&gt;non-isolated infrastructure&lt;/li&gt;
&lt;li&gt;hidden dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution is not “rerun CI.”&lt;/p&gt;

&lt;p&gt;The solution is making tests deterministic.&lt;/p&gt;

&lt;p&gt;A reliable test suite should produce the same result every time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;locally&lt;/li&gt;
&lt;li&gt;in CI&lt;/li&gt;
&lt;li&gt;on every machine&lt;/li&gt;
&lt;li&gt;under every execution order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once your tests become deterministic, your entire development workflow becomes faster, safer, and dramatically less frustrating.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>laravel</category>
      <category>php</category>
      <category>testing</category>
    </item>
    <item>
      <title>How to Deploy 10 Times a Day Safely with Feature Flags</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Sun, 07 Jun 2026 15:49:08 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/how-to-deploy-10-times-a-day-safely-with-feature-flags-3kln</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/how-to-deploy-10-times-a-day-safely-with-feature-flags-3kln</guid>
      <description>&lt;p&gt;If you’ve been following my previous posts, you know I’m a big advocate for Trunk-Based Development and shrinking your pull requests until they almost feel too small. In a perfect world, developers merge code directly into the main branch multiple times a day, everything flows smoothly, and production remains rock solid.&lt;/p&gt;

&lt;p&gt;But let’s be honest. When you actually try to pitch this to a backend team working on a core system, you almost always hit the exact same wall of resistance.&lt;/p&gt;

&lt;p&gt;Someone in the back of the room will inevitably raise their hand and ask: &lt;em&gt;“That sounds great in theory, but I’m currently refactoring our legacy checkout service. It’s going to take me four days of deep architectural changes. Are you seriously telling me I should merge half-baked, broken code into the main trunk and push it straight to production where real customers are buying our products?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It’s a completely valid objection. If your only tool for hiding uncompleted work is holding onto a massive, long-lived feature branch, then trunk-based development breaks down immediately. You end up with the exact nightmare we talked about earlier: huge code reviews, painful merge conflicts, and code that rots before it ever sees a live environment.&lt;/p&gt;

&lt;p&gt;To make continuous delivery actually work without causing catastrophic production outages every single afternoon, you need to decouple two concepts that most engineering teams mistakenly treat as the exact same thing: &lt;strong&gt;Deployment&lt;/strong&gt; and &lt;strong&gt;Release&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Last article in this category is focused on Trunk-Based Development: &lt;a href="https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/05/18/trunk-based-development-roadmap/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Concept: Shifting Left by Decoupling
&lt;/h2&gt;

&lt;p&gt;In traditional development setups, deploying code and releasing a feature happen simultaneously. You merge your giant feature branch, the CI/CD pipeline runs, the code hits the live servers, and boom—your users immediately see the new functionality.&lt;/p&gt;

&lt;p&gt;This model is incredibly high-stakes. If something goes wrong, your only options are rolling back the entire deployment (which might contain unrelated fixes from other developers) or rushing a frantic hotfix through the pipeline while customer support tickets pile up and management starts breathing down your neck.&lt;/p&gt;

&lt;p&gt;Feature flags (or feature toggles) completely change this dynamic by shifting the risk layout.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Deployment means moving bits to servers. Your code lives in the production environment, executing safely under the hood, but it remains invisible or inaccessible to the end user. It’s a technical activity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Release means making that code active for users. It’s a business decision, completely independent of the deployment schedule.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By wrapping your new code inside a simple conditional statement, you can safely deploy unfinished logic to production ten times a day. The code is physically there on your production servers, but the execution path is dormant. You’ve successfully removed the stress from the deployment process.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Realistic Look at the Code
&lt;/h2&gt;

&lt;p&gt;Let’s skip the over-engineered enterprise frameworks for a moment and look at how this actually plays out in a standard backend context. Imagine you are upgrading a legacy payment gateway integration to a new, more reliable third-party API provider.&lt;/p&gt;

&lt;p&gt;Instead of waiting weeks to swap the entire implementation out in one massive, terrifying PR, you introduce a flag. In its simplest form, it looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentProcessor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;NewPaymentGateway&lt;/span&gt; &lt;span class="n"&gt;newGateway&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;LegacyPaymentGateway&lt;/span&gt; &lt;span class="n"&gt;legacyGateway&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;FeatureFlagClient&lt;/span&gt; &lt;span class="n"&gt;flagClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flagClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"use-new-payment-gateway"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUserId&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;newGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;legacyGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Fallback safety net&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flagClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFeatureEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"use-new-payment-gateway"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUserId&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"New gateway failed, falling back to legacy for user: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUserId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;legacyGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we aren't just checking a global true/false boolean config value. We are passing the order.getUserId() into the flag client. This allows for runtime evaluation based on context.&lt;/p&gt;

&lt;p&gt;With this setup, you can merge your new gateway code when it’s only 20% finished. The interface is there, the basic structure is set, but the flag is turned off for everyone in production. You get to test your integration continuously against real staging environments or hidden production paths without risking a single actual customer transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Database Problem: Handling Migrations Safely
&lt;/h2&gt;

&lt;p&gt;One common argument against frequent deployments with feature flags is: &lt;em&gt;“What about database changes? You can’t just feature-flag a schema migration.”&lt;/em&gt; This is where many teams stumble. If your code depends on a new database column that doesn't exist yet, your application will crash. To solve this, your database strategy must evolve alongside your code isolation. You have to follow the &lt;strong&gt;Expand and Contract&lt;/strong&gt; pattern.&lt;/p&gt;

&lt;p&gt;Instead of renaming or modifying a column in a single destructive step, you break the change down into multiple backward-compatible deployments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Expand: You deploy a migration that adds the new column or table. The old code doesn't know it exists, so nothing breaks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dual Write: You deploy a feature flag that starts writing data to both the old and new columns simultaneously, but still reads only from the old one. This ensures your new schema populates with live data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Backfill: You run a background script to copy historical data from the old structure to the new one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flip the Switch: You change the feature flag to read from the new column. If performance degrades, you slide the flag back instantly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Contract: Once you are 100% confident, you remove the feature flag, delete the old code path, and deploy a final migration to drop the old column.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Yes, it requires more steps. But it transforms a terrifying database migration into a series of boring, completely safe tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving Beyond Simple Booleans: Dark Launching and Canaries
&lt;/h2&gt;

&lt;p&gt;Once you separate deployment from release, you unlock deployment workflows that make standard staging environments look completely obsolete. The most powerful of these is the &lt;strong&gt;Canary Release&lt;/strong&gt; (or gradual rollout).&lt;/p&gt;

&lt;p&gt;Instead of flipping a switch and hoping your database doesn't melt under a new query load, you can configure your feature flag system to evaluate based on percentages or specific user attributes. A realistic rollout plan for our new payment gateway looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Internal Testing (The QA Tier)&lt;/strong&gt;&lt;br&gt;
The flag is enabled only for internal QA team user IDs or specifically whitelisted corporate IP addresses. You are running tests on the live production infrastructure, using real database connections, but nobody outside your company knows about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: The Canary (1% Traffic)&lt;/strong&gt;&lt;br&gt;
You route exactly 1% of random global traffic through the new gateway. You sit back and monitor your logging dashboard for an hour. You look for spikes in 500 errors, increased latency, or unusual database connection pool exhaustion. If 1% of your users experience a bug, it’s a minor issue you can catch quickly, rather than a company-wide outage affecting everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: The Ramp-Up (10% -&amp;gt; 50%)&lt;/strong&gt;&lt;br&gt;
If the metrics look clean after 24 hours, you scale the flag to 10%, then 50% over the next two days. This gradual increase helps you see how the system behaves under a realistic load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Full Release (100%)&lt;/strong&gt;&lt;br&gt;
The feature is stable, metrics are perfect, and the old legacy gateway is officially ready for decommissioning.&lt;/p&gt;

&lt;p&gt;If a subtle edge-case bug appears when you hit the 10% mark, you don’t panic. You don’t trigger a full rollback of the service container, which might take 15 minutes to compile and deploy. You simply log into your feature flag dashboard, slide the toggle back to 0%, and fix the bug at your own pace during normal working hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dark Side: Managing the Architecture Debt
&lt;/h2&gt;

&lt;p&gt;If you talk to any backend engineer who has used feature flags in a messy, fast-moving project, they will warn you about the exact same thing: technical debt. It is incredibly easy to treat feature flags like a magic wand, scattering them everywhere until your codebase looks like a tangled bowl of conditional spaghetti.&lt;/p&gt;

&lt;p&gt;If a flag stays in your code for six months after a feature has rolled out to 100% of users, it stops being a tool for continuous delivery and becomes an architectural liability. It makes the code harder to read, complicates unit testing because you have to mock multiple flag states, and leaves dead code paths hanging around indefinitely.&lt;/p&gt;

&lt;p&gt;To prevent your system from turning into an unmaintainable maze, you need to establish strict engineering discipline around the lifecycle of a flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat Toggles as Temporary Scaffolding
&lt;/h2&gt;

&lt;p&gt;Every time you create a feature flag, you should immediately create a corresponding ticket in your backlog to remove that flag. The definition of done for a new feature shouldn't just be "it works in production." It must be "it works in production, the old legacy code is deleted, and the flag conditional is completely stripped out of the codebase."&lt;/p&gt;

&lt;h2&gt;
  
  
  Flag Owner Assignment
&lt;/h2&gt;

&lt;p&gt;Every flag must have a clear owner—either a specific developer or a product team. If a flag sits unchanged for more than four weeks, the automated system or team lead should trigger a warning to review its status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Them Short-Lived
&lt;/h2&gt;

&lt;p&gt;Release toggles should rarely live longer than a single development sprint or two. If a flag has been at 100% for more than a few days without complaints, it’s time to schedule a quick cleanup PR. Don’t let them turn into permanent configuration settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Your Tools Safely
&lt;/h2&gt;

&lt;p&gt;You don't need to build a massive, complex internal configuration platform from scratch to get started. For smaller teams, a simple, centralized database table or a Redis-backed configuration file that reloads dynamically can be enough to get your feet wet.&lt;/p&gt;

&lt;p&gt;As your team expands and you need advanced targeting rules, percentage rollouts, and audit logs, looking at dedicated tools like LaunchDarkly, Flagsmith, or open-source solutions like Unleash becomes highly valuable.&lt;/p&gt;

&lt;p&gt;The critical architectural requirement is that evaluating a flag must be lightning-fast. It cannot introduce a blocking HTTP request into your critical backend path every time a function is called; it needs to resolve locally in memory via cached flag states that sync asynchronously in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Transitioning to a workflow where you deploy to production ten times a day isn’t an engineering flex—it’s about reducing anxiety. It changes the entire culture of a development team. Production deployments stop being high-stress, late-night events that require everyone to be on standby with their laptops open. They become boring, routine non-events that happen continuously in the background while you grab a coffee or focus on your next task.&lt;/p&gt;

&lt;p&gt;Feature flags are the missing link that makes this possible. They give you the safety net to keep your pull requests tiny, your main branch green, and your delivery pipeline moving forward without ever breaking the user experience.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>devops</category>
      <category>software</category>
    </item>
    <item>
      <title>State Pattern vs. Enums in Modern PHP</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 26 May 2026 15:06:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/state-pattern-vs-enums-in-modern-php-2oeg</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/state-pattern-vs-enums-in-modern-php-2oeg</guid>
      <description>&lt;p&gt;In many PHP and Laravel applications, entity lifecycles start simple. An Order can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pending&lt;/li&gt;
&lt;li&gt;Paid&lt;/li&gt;
&lt;li&gt;Shipped&lt;/li&gt;
&lt;li&gt;Cancelled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For simple workflows, Enums are often exactly the right solution. The problem begins when states stop being just labels and start accumulating behavior.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Previous articlet in Refactoring cattegory: &lt;a href="https://codecraftdiary.com/2026/05/02/mastering-value-objects-in-php/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/05/02/mastering-value-objects-in-php/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enums Are Excellent — Until They Aren’t
&lt;/h2&gt;

&lt;p&gt;For simple workflows, Enums are clean and maintainable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Shipped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is ideal when states are primarily used for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filtering and querying data&lt;/li&gt;
&lt;li&gt;Display logic and badges in UI&lt;/li&gt;
&lt;li&gt;Basic validation&lt;/li&gt;
&lt;li&gt;API serialization&lt;/li&gt;
&lt;li&gt;Database persistence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Problems start appearing when business rules become state-dependent. A common first step is adding helper methods directly into the Enum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Shipped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;canBeCancelled&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Shipped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Cancelled&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still perfectly reasonable and respects the KISS principle.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;At that point, the Enum slowly stops being a simple value object and starts becoming a workflow engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Problem: Growing Coupling
&lt;/h2&gt;

&lt;p&gt;Consider how a bloating Enum typically looks in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Shipped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;PaymentGateway&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;InventoryManager&lt;/span&gt; &lt;span class="nv"&gt;$inventory&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Pending&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;executeCancellationWithRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$inventory&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Shipped&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LogicException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Cannot cancel a shipped order.'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Cancelled&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LogicException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Order is already cancelled.'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;executeCancellationWithRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;PaymentGateway&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;InventoryManager&lt;/span&gt; &lt;span class="nv"&gt;$inventory&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;restock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issue here is not the number of lines. &lt;strong&gt;The issue is coupling.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Enum now knows about payment infrastructure, inventory management, business transitions, and side effects. Adding a new state such as &lt;em&gt;PartiallyRefunded&lt;/em&gt; now requires modifying a growing conditional structure that centralizes unrelated responsibilities.&lt;/p&gt;

&lt;p&gt;This is where applications experience state explosion—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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The State Pattern: Isolating Behavior
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Define the Workflow Contract&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;OrderState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;toValue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interface should stay minimal. Only include operations whose behavior actually changes depending on the state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Create Small, Focused State Classes&lt;/strong&gt;&lt;br&gt;
Each state becomes an isolated, testable component. Look at how clean the responsibilities become:&lt;/p&gt;

&lt;p&gt;Pending State&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PendingState&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;OrderState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CancelledState&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LogicException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Cannot ship an unpaid order.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;toValue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paid State&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaidState&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;OrderState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;PaymentGateway&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;InventoryManager&lt;/span&gt; &lt;span class="nv"&gt;$inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;restock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CancelledState&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ShippedState&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;toValue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shipped State&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShippedState&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;OrderState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LogicException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The order has already been shipped.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LogicException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Order is already shipped.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;toValue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most importantly, adding a new state no longer requires modifying a massive conditional block. This aligns naturally with the &lt;strong&gt;Open/Closed Principle.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A Pragmatic Hybrid Approach&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Completely replacing Enums with raw state objects is often unnecessary in database-driven applications. In practice, the most maintainable approach is a hybrid architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enums handle persistence, transport, and API serialization.&lt;/li&gt;
&lt;li&gt;State objects handle business behavior and side effects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Enum remains the canonical storage format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;OrderStatusEnum&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Shipped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resolving State Behavior Cleanly
&lt;/h2&gt;

&lt;p&gt;Instead of resolving dependencies directly inside the model, a dedicated factory keeps infrastructure concerns isolated from your domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderStateFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;PaymentGateway&lt;/span&gt; &lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;InventoryManager&lt;/span&gt; &lt;span class="nv"&gt;$inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;OrderStatusEnum&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;OrderState&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;OrderStatusEnum&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Pending&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PendingState&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;OrderStatusEnum&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Paid&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PaidState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;OrderStatusEnum&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Shipped&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ShippedState&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;OrderStatusEnum&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Cancelled&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CancelledState&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Keeping the Model Lightweight
&lt;/h2&gt;

&lt;p&gt;Now, the &lt;em&gt;Order&lt;/em&gt; model stays clean and decoupled from infrastructure services. It simply orchestrates the workflow via the factory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;OrderStatusEnum&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$payment_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;OrderStateFactory&lt;/span&gt; &lt;span class="nv"&gt;$stateFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;transitionTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;OrderState&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderStatusEnum&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toValue&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stateFactory&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ship&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stateFactory&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps your persistence simple, business logic isolated, dependencies explicit, and workflows extensible—all without introducing heavy framework abstractions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Becomes Significantly Easier
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PendingStateTest — Verify that cancellation transitions directly to cancelled.&lt;/li&gt;
&lt;li&gt;PaidStateTest — Assert that the payment gateway receives the refund call and inventory is restocked.&lt;/li&gt;
&lt;li&gt;ShippedStateTest — Assert that exceptions are thrown correctly on forbidden actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This dramatically reduces test setup complexity and makes transition rules much easier to verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should You Use Enums vs. State Objects?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use Enums when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The state is primarily a static label.&lt;/li&gt;
&lt;li&gt;Transitions are simple and linear.&lt;/li&gt;
&lt;li&gt;Behavior differences between states are minimal.&lt;/li&gt;
&lt;li&gt;No external services or infrastructure are involved.&lt;/li&gt;
&lt;li&gt;Examples: Blog post status (Draft, Published), user visibility flags, or filtering categories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use the State Pattern when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transitions trigger side effects or external APIs.&lt;/li&gt;
&lt;li&gt;States require completely different validation rules.&lt;/li&gt;
&lt;li&gt;Workflows keep expanding with new edge cases.&lt;/li&gt;
&lt;li&gt;Conditional logic (match or if/else) around the same state starts repeating across the codebase.&lt;/li&gt;
&lt;li&gt;Examples: Payment workflows, fulfillment systems, subscription lifecycles, or approval pipelines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Enums are not the enemy. In fact, they are often the best solution for simple state representation.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;responsibility density&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>development</category>
      <category>software</category>
      <category>php</category>
    </item>
    <item>
      <title>Trunk-Based Development: From Chaos to Flow</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 19 May 2026 14:16:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/trunk-based-development-from-chaos-to-flow-4mkg</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/trunk-based-development-from-chaos-to-flow-4mkg</guid>
      <description>&lt;p&gt;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 “Short-lived Feature Branching” 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.&lt;/p&gt;

&lt;p&gt;But how do you actually fix it? Knowing small PRs are better is easy. Changing a team’s habits is the hard part.&lt;br&gt;
This is the practical roadmap for moving from long-lived branches to real trunk-based flow.&lt;/p&gt;

&lt;p&gt;Previous posts about Trunk-Based Development: &lt;strong&gt;Pt. 2&lt;/strong&gt; — &lt;a href="https://codecraftdiary.com/2026/04/29/trunk-based-development-your-pull-requests-are-still-too-big/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/04/29/trunk-based-development-your-pull-requests-are-still-too-big/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pt. 1&lt;/strong&gt; &lt;a href="https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/04/04/trunk-based-development-why-most-teams-think-they-use-it-but-dont/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Fixing the Foundation (The Infrastructure)
&lt;/h2&gt;

&lt;p&gt;You cannot do trunk-based development with slow pipelines. If your CI/CD suite is a bottleneck, your developers will naturally revert to batching work to “save time”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The 10-Minute Rule for CI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In 2026, speed is a requirement, not a luxury. If your CI takes 20 minutes, there is friction; if it takes 60 minutes, people stop merging frequently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target: Aim for CI under 10 minutes.&lt;/li&gt;
&lt;li&gt;Action: Parallelize your test suites. If a test is flaky, don’t ignore it — fix it or delete it. A flaky test suite is a debt that destroys the confidence needed for frequent merges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Radical Observability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To merge to main multiple times a day safely, you need to know exactly what is happening in production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Action: 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Phase 2: Mastering the Tools of “Incomplete” Work
&lt;/h2&gt;

&lt;p&gt;The biggest fear in TBD is merging work that isn’t “done”. To overcome this, you must decouple deployment from release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Feature Flags as a Standard&lt;/strong&gt;&lt;br&gt;
Feature flags are the missing piece for most teams. They allow you to merge partial work and control exposure without waiting for the entire feature to be polished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Strategy:&lt;/strong&gt; Wrap new logic in a toggle. This lets the code live in the main branch, deployed to production but hidden from users until it’s ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Rule:&lt;/strong&gt; A feature flag must exist from the start, not as an afterthought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Branch by Abstraction for Large Changes&lt;/strong&gt;&lt;br&gt;
When you are doing a complete architectural overhaul, do not create a “v2-architecture” Git branch. That is a recipe for a merge nightmare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Strategy:&lt;/strong&gt; Keep both the old and new architectures in the main branch simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Execution:&lt;/strong&gt; Use an abstraction layer (an interface or wrapper) to toggle between the old and new logic. Run “dark launches” where the new code executes, but you ignore the results or simply compare them against the old version to gain confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: Rewiring the Process (Small PRs)
&lt;/h2&gt;

&lt;p&gt;Small pull requests are the backbone of TBD. A large PR is a cognitive nightmare that leads to shallow reviews and delayed deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Enforce Hard PR Size Limits&lt;/strong&gt;&lt;br&gt;
Don’t make small PRs a suggestion; make them a constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Rule:&lt;/strong&gt; Set a soft limit of ~300 lines and a hard limit of ~400–500 lines per PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Logic:&lt;/strong&gt; Constraints force better behavior. If a task is too big, it forces the developer to think about how to slice it vertically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Vertical vs. Horizontal Slicing&lt;/strong&gt;&lt;br&gt;
Stop splitting work by “Backend PR,” “Database PR,” and “Frontend PR”. This creates artificial dependencies and forces you to wait until all are done before merging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Strategy:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4: The Cultural Shift (The Hard Part)
&lt;/h2&gt;

&lt;p&gt;Trunk-based development is 10% tooling and 90% discipline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Optimize for Review Speed, Not Just Quality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If reviews take days, developers will batch work to avoid the “waiting tax”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expectation:&lt;/strong&gt; A PR should be reviewed within a few hours, not days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technique:&lt;/strong&gt; If a PR is small (under 300 – 400 lines), it takes 15 minutes to review. If it’s still stuck, do live reviews or pair programming to clear the logjam.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Accept “Ugly but Correct”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Flow matters more than perfection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Mindset:&lt;/strong&gt; It is better to merge a small, slightly imperfect (but safe) change today than a “perfect” massive change next week. You can refactor and improve incrementally once the code is integrated -&amp;gt; but don’t forget to refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 5: The Cleanup and AI Reality
&lt;/h2&gt;

&lt;p&gt;AI-assisted coding is generating more code than ever. If you don’t have small changes and fast integration, your workflow will collapse under the sheer volume of AI-generated changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. The Non-Optional Cleanup&lt;/strong&gt;&lt;br&gt;
Feature flags and abstractions are great, but they create technical debt if left forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Rule:&lt;/strong&gt; Once a rollout is 100% successful, deleting the old code and the flag is part of the original task, not a “nice-to-have” for later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Track Your Behavior (Metrics)&lt;/strong&gt;&lt;br&gt;
Stop guessing if you are doing TBD. Measure it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KPIs:&lt;/strong&gt; Track average PR size, PR lifetime, and merges per developer per day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reality Check:&lt;/strong&gt; If your PRs live for days and contain thousands of lines, you are still doing feature-branch development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common Failure Mode&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Teams adopt feature flags but still keep long-lived branches.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;That is not trunk-based development.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The branch lifetime matters more than the branching strategy itself.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Trunk-based development feels uncomfortable at first because it goes against the natural desire to be “complete” and “polished” before sharing work. It requires you to prioritize the system’s flow over your individual comfort.&lt;/p&gt;

&lt;p&gt;If you fix just one thing this month, make your pull requests radically smaller. Faster reviews, fewer bugs, and smoother delivery will follow. Everything else is just details.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>software</category>
      <category>devops</category>
    </item>
    <item>
      <title>How Mutation Testing Exposes the Truth (PHP 2026 Edition)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 12 May 2026 13:50:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/how-mutation-testing-exposes-the-truth-php-2026-edition-1m1l</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/how-mutation-testing-exposes-the-truth-php-2026-edition-1m1l</guid>
      <description>&lt;p&gt;You'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?&lt;/p&gt;

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

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/04/18/laravel-testing-mistakes/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/04/18/laravel-testing-mistakes/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dirty Secret of Code Coverage
&lt;/h2&gt;

&lt;p&gt;Code coverage tells you which lines were &lt;em&gt;executed&lt;/em&gt; during your test run. It says nothing about whether your assertions are actually meaningful.&lt;/p&gt;

&lt;p&gt;Consider this classic trap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderDiscountCalculator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$quantity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a test that covers it 100%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;PHPUnit\Framework\TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderDiscountCalculatorTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testCalculate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$calculator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderDiscountCalculator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Both branches hit — 100% coverage!&lt;/span&gt;
        &lt;span class="nv"&gt;$calculator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$calculator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test covers 100% of the code. It also asserts absolutely nothing. If someone changes &lt;code&gt;0.9&lt;/code&gt; to &lt;code&gt;0.5&lt;/code&gt;, your test suite stays green while your customers get 50% off everything. That's a very expensive bug.&lt;/p&gt;

&lt;p&gt;This is precisely the problem mutation testing solves.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Mutation Testing?
&lt;/h2&gt;

&lt;p&gt;Mutation testing works by automatically introducing small bugs — called &lt;strong&gt;mutants&lt;/strong&gt; — into your source code, then running your test suite against each mutated version. If your tests catch the bug (the mutant is &lt;strong&gt;killed&lt;/strong&gt;), great. If your tests still pass with the bug in place (the mutant &lt;strong&gt;survives&lt;/strong&gt;), you have a gap.&lt;/p&gt;

&lt;p&gt;Common mutations include things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changing &lt;code&gt;&amp;gt;=&lt;/code&gt; to &lt;code&gt;&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replacing &lt;code&gt;+&lt;/code&gt; with &lt;code&gt;-&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Flipping &lt;code&gt;true&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Removing entire &lt;code&gt;return&lt;/code&gt; statements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The metric you care about is the &lt;strong&gt;Mutation Score Indicator (MSI)&lt;/strong&gt; — the percentage of mutants your tests kill. A high MSI means your tests are genuinely sensitive to regressions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started with Infection PHP
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://infection.github.io/" rel="noopener noreferrer"&gt;Infection&lt;/a&gt; is the de facto mutation testing framework for PHP. It integrates cleanly with PHPUnit and runs as a Composer dev dependency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; infection/infection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it for the first time with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./vendor/bin/infection &lt;span class="nt"&gt;--threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Infection will run your existing test suite, then start generating and testing mutants. On a modern project with &lt;code&gt;--threads=4&lt;/code&gt;, it's fast enough to include in a CI pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Real-World Example: Catching What Coverage Misses
&lt;/h2&gt;

&lt;p&gt;Let me walk you through a scenario I actually encountered on a SaaS project — a pricing engine with tiered discounts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TieredPricingService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;TIERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 30% discount for 100+&lt;/span&gt;
        &lt;span class="mi"&gt;50&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 20% discount for 50+&lt;/span&gt;
        &lt;span class="mi"&gt;10&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 10% discount for 10+&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$unitPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$quantity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TIERS&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$minQuantity&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$multiplier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nv"&gt;$minQuantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$unitPrice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$multiplier&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$unitPrice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Here's what the corrected test looked like after Infection exposed the gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;PHPUnit\Framework\TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;PHPUnit\Framework\Attributes\DataProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TieredPricingServiceTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;TieredPricingService&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TieredPricingService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;#[DataProvider('pricingProvider')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testGetPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$unitPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$unitPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$quantity&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;pricingProvider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'below first tier'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="mf"&gt;50.00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'exactly at 10 tier'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;90.00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// boundary — was missing!&lt;/span&gt;
            &lt;span class="s1"&gt;'above 10 tier'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;99.00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'exactly at 50 tier'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;400.00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// boundary&lt;/span&gt;
            &lt;span class="s1"&gt;'exactly at 100 tier'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;700.00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// boundary&lt;/span&gt;
            &lt;span class="s1"&gt;'above highest tier'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1400.00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After adding boundary assertions, Infection's MSI jumped from 61% to 94%. That's the difference between a test suite that gives you false confidence and one that actually has your back.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuring Infection for Your Project
&lt;/h2&gt;

&lt;p&gt;Infection is configured via &lt;code&gt;infection.json5&lt;/code&gt; in your project root. Here's a production-ready config I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "$schema": "vendor/infection/infection/resources/schema.json",
    "source": {
        "directories": ["src"],
        "excludes": ["src/Infrastructure/Migrations"]
    },
    "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"
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;minMsi&lt;/code&gt; and &lt;code&gt;minCoveredMsi&lt;/code&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Integrating Into CI (GitHub Actions)
&lt;/h2&gt;

&lt;p&gt;Here's a GitHub Actions job I've been running since mid-2025:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mutation-testing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup PHP&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shivammathur/setup-php@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;php-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8.3'&lt;/span&gt;
        &lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;xdebug&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer install --no-interaction&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Infection&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./vendor/bin/infection --threads=4 --min-msi=85 --min-covered-msi=90&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Objections — And Honest Answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"It's too slow."&lt;/strong&gt; It can be on large codebases, but &lt;code&gt;--threads&lt;/code&gt; and configuring &lt;code&gt;source.excludes&lt;/code&gt; to skip generated code, migrations, and DTOs makes a huge difference. I typically exclude everything that has no business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The MSI is too low to be useful."&lt;/strong&gt; Start with &lt;code&gt;--min-msi=0&lt;/code&gt; and just look at the HTML report. Prioritize killing mutants in your core domain logic first — that's where bugs actually hurt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It produces too many surviving mutants."&lt;/strong&gt; Some mutants are genuinely equivalent (they don't change behavior). Infection lets you mark these as ignored in config. Over time your noise floor drops significantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What My Workflow Looks Like in 2026
&lt;/h2&gt;

&lt;p&gt;My current approach on active PHP projects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PHPUnit with strict coverage&lt;/strong&gt; for the fast feedback loop during development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infection on every PR&lt;/strong&gt; targeting only changed files — using &lt;code&gt;--git-diff-filter&lt;/code&gt; (available since Infection 0.27) to keep CI times reasonable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Infection run weekly&lt;/strong&gt; on the &lt;code&gt;main&lt;/code&gt; branch to catch gradual MSI drift.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;--git-diff-filter&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./vendor/bin/infection &lt;span class="nt"&gt;--git-diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AM &lt;span class="nt"&gt;--threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;protected&lt;/em&gt; by tests that would actually catch a regression.&lt;/p&gt;

&lt;p&gt;If you're publishing technical content in 2026 and you're not talking about mutation testing, you're leaving one of PHP'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.&lt;/p&gt;

&lt;p&gt;Start with a single service class. Run Infection. Look at what survives. I promise you'll find something surprising.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://infection.github.io/guide/" rel="noopener noreferrer"&gt;Infection PHP documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/infection/infection" rel="noopener noreferrer"&gt;Infection GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://phpunit.de/" rel="noopener noreferrer"&gt;PHPUnit documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>php</category>
      <category>software</category>
    </item>
  </channel>
</rss>
