<?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>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>
    <item>
      <title>Mastering Value Objects in PHP 8.5+ (2026 Edition)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 05 May 2026 14:01:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/mastering-value-objects-in-php-85-2026-edition-126g</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/mastering-value-objects-in-php-85-2026-edition-126g</guid>
      <description>&lt;p&gt;As developers, we often have a problematic relationship with primitives. We use a string for an email, a float for a price, and an int for a status. This is what we call Primitive Obsession—and it’s one of the common reasons why PHP codebases gradually become hard to maintain.&lt;/p&gt;

&lt;p&gt;If you’ve been following my series on Refactoring &amp;amp; Patterns, you know I’m a fan of the Introduce Parameter Object pattern. But today, I want to go deeper and talk about one of the smallest, yet most powerful building blocks of clean architecture: ** Value Objects**.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/04/11/fat-controller-laravel-refactor/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Price” of Primitive Obsession
&lt;/h2&gt;

&lt;p&gt;Imagine you’re working on an e-commerce platform. You have a Product and a Discount.&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;applyDiscount&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;float&lt;/span&gt; &lt;span class="nv"&gt;$discountPercentage&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;$discountPercentage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$discountPercentage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&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;InvalidArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Invalid discount"&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="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$discountPercentage&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this looks fine. But in a real-world application, that $price is floating around (pun intended) everywhere.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it USD or EUR?&lt;/li&gt;
&lt;li&gt;Does it include VAT?&lt;/li&gt;
&lt;li&gt;What about rounding?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And more importantly: what happens if you accidentally pass &lt;em&gt;$discountPercentage&lt;/em&gt; as &lt;em&gt;$price&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;PHP won’t complain. Both are floats. You just sold a MacBook for $15.&lt;/p&gt;

&lt;p&gt;On top of that, floats introduce precision issues, which makes them a poor choice for financial calculations in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Exactly is a Value Object?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Value Object (VO)&lt;/strong&gt; is an object that is defined by its value rather than its identity.&lt;/p&gt;

&lt;p&gt;Two Value Objects with the same data are considered equal—even if they are different instances.&lt;/p&gt;

&lt;p&gt;In modern PHP (8.2+), a well-designed Value Object has three key characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Immutability – once created, it cannot change&lt;/li&gt;
&lt;li&gt;Validation – it cannot exist in an invalid state&lt;/li&gt;
&lt;li&gt;Self-documentation – the type clearly expresses intent&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Better Approach: Explicit Domain Types
&lt;/h2&gt;

&lt;p&gt;Let’s refactor the previous 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;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Price&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;int&lt;/span&gt; &lt;span class="nv"&gt;$amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// in cents&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;Currency&lt;/span&gt; &lt;span class="nv"&gt;$currency&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&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;InvalidPriceException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Price cannot be negative."&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Price&lt;/span&gt; &lt;span class="nv"&gt;$other&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Price&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$other&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&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;CurrencyMismatchException&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Price&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;amount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$other&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;amount&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;currency&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;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Price&lt;/span&gt; &lt;span class="nv"&gt;$other&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$other&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;currency&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$other&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;currency&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;A few important things are happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encapsulation – price logic lives inside the Price class&lt;/li&gt;
&lt;li&gt;Type safety – you cannot mix currencies accidentally&lt;/li&gt;
&lt;li&gt;Immutability – every operation returns a new instance&lt;/li&gt;
&lt;li&gt;Precision – using integers avoids float rounding issues&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters (Especially Today)
&lt;/h2&gt;

&lt;p&gt;With AI-assisted development becoming standard, types matter more than ever.&lt;/p&gt;

&lt;p&gt;When you use primitives, tools like GitHub Copilot or ChatGPT have to guess intent.&lt;/p&gt;

&lt;p&gt;When you use a Price or EmailAddress object, both humans and AI can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understand constraints immediately&lt;/li&gt;
&lt;li&gt;discover available behavior via methods&lt;/li&gt;
&lt;li&gt;avoid invalid states by design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’re not just writing code—you’re defining a &lt;strong&gt;clear contract&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Refactoring: Email
&lt;/h2&gt;

&lt;p&gt;How often have you written this?&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;filter_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FILTER_VALIDATE_EMAIL&lt;/span&gt;&lt;span class="p"&gt;))&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;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Invalid email"&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;If it appears in multiple places, that’s duplication—and a maintenance risk.&lt;/p&gt;

&lt;p&gt;Let’s move that logic into a Value Object:&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;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailAddress&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;string&lt;/span&gt; &lt;span class="nv"&gt;$value&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;string&lt;/span&gt; &lt;span class="nv"&gt;$value&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="nb"&gt;filter_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FILTER_VALIDATE_EMAIL&lt;/span&gt;&lt;span class="p"&gt;))&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;InvalidEmailException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&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;getDomain&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="nb"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strrchr&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"@"&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;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__toString&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;value&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;Now your service layer becomes much cleaner:&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;// BEFORE&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;registerUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$password&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="c1"&gt;// AFTER&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;registerUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;EmailAddress&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Password&lt;/span&gt; &lt;span class="nv"&gt;$password&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment execution reaches registerUser, you already know the email is valid.&lt;/p&gt;

&lt;p&gt;Validation is handled at the boundary of your system—not scattered across your codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logic-Heavy Value Objects
&lt;/h2&gt;

&lt;p&gt;A common mistake is treating Value Objects as simple data containers.&lt;/p&gt;

&lt;p&gt;In practice, they should encapsulate &lt;strong&gt;behavior related to that data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of passing:&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;string&lt;/span&gt; &lt;span class="nv"&gt;$startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$endDate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderDateRange
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With methods like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;overlapsWith()&lt;/li&gt;
&lt;li&gt;isWithinLastMonth()&lt;/li&gt;
&lt;li&gt;getDurationInDays()&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduces cognitive load in your services and keeps domain logic where it belongs.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Use Value Objects
&lt;/h2&gt;

&lt;p&gt;Not everything needs to be a Value Object.&lt;/p&gt;

&lt;p&gt;Ask yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this data have validation rules?&lt;/li&gt;
&lt;li&gt;Is it reused in multiple places?&lt;/li&gt;
&lt;li&gt;Does it represent a domain concept (SKU, IBAN, Email, Price)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is yes, a Value Object is likely justified.&lt;/p&gt;

&lt;p&gt;If you’re building a quick prototype, primitives are fine. Just be aware of the trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;A common concern used to be performance—creating many small objects instead of using primitives.&lt;/p&gt;

&lt;p&gt;In modern PHP, object instantiation is highly optimized. The overhead is negligible compared to the cost of bugs caused by invalid states.&lt;/p&gt;

&lt;p&gt;More importantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;immutable objects are predictable&lt;/li&gt;
&lt;li&gt;they eliminate side effects&lt;/li&gt;
&lt;li&gt;they are naturally safe in concurrent or async contexts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Refactoring toward Value Objects is one of the most effective ways to improve code quality.&lt;/p&gt;

&lt;p&gt;It forces you to think in terms of &lt;strong&gt;domain concepts&lt;/strong&gt;, not just data types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical steps:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Look at a complex service class&lt;/li&gt;
&lt;li&gt;Find a variable validated in multiple places&lt;/li&gt;
&lt;li&gt;Extract it into a readonly Value Object&lt;/li&gt;
&lt;li&gt;Move related logic into that object&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ll end up with code that is easier to read, safer to modify, and harder to break.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>php</category>
      <category>refactorit</category>
      <category>software</category>
    </item>
    <item>
      <title>Trunk-Based Development: Your Pull Requests Are Still Too Big</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Thu, 30 Apr 2026 13:18:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/trunk-based-development-your-pull-requests-are-still-too-big-3jcj</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/trunk-based-development-your-pull-requests-are-still-too-big-3jcj</guid>
      <description>&lt;p&gt;Most teams don’t realize this, but their biggest bottleneck isn’t architecture, tech stack, or even legacy code.&lt;/p&gt;

&lt;p&gt;It’s pull requests.&lt;/p&gt;

&lt;p&gt;If you read about trunk-based development, you’ll see the same advice repeated everywhere: small changes, frequent merges, fast feedback. Sounds simple. Almost obvious.&lt;/p&gt;

&lt;p&gt;And yet — in reality — most teams are nowhere near that.&lt;/p&gt;

&lt;p&gt;I’ve personally seen pull requests sitting open for months. Not days. Not weeks. Months.&lt;/p&gt;

&lt;p&gt;At one point, we had a pull request in our team that was open for more than &lt;strong&gt;six months&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Six&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Months&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At that point, it’s no longer a pull request. It’s a parallel universe.&lt;/p&gt;

&lt;p&gt;Below this link you can read first article about Trunk-Based Development &lt;/p&gt;

&lt;p&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;
  
  
  The Hidden Cost of Large Pull Requests
&lt;/h2&gt;

&lt;p&gt;Let’s be precise about what’s happening here.&lt;/p&gt;

&lt;p&gt;A large pull request is not just “a bit harder to review”. It fundamentally breaks your delivery system.&lt;/p&gt;

&lt;p&gt;Here’s what actually happens:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Review becomes a cognitive nightmare&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a PR has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,000+ lines&lt;/li&gt;
&lt;li&gt;multiple concerns (API, DB, UI, validation)&lt;/li&gt;
&lt;li&gt;partial refactors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No one can realistically review it properly.&lt;/p&gt;

&lt;p&gt;So what happens?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reviewers skim&lt;/li&gt;
&lt;li&gt;they miss edge cases&lt;/li&gt;
&lt;li&gt;they delay the review (“I’ll look at it later”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And “later” turns into never.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Feedback loop collapses&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fast feedback is the core of modern development.&lt;/p&gt;

&lt;p&gt;But with large PRs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feedback comes late&lt;/li&gt;
&lt;li&gt;feedback is vague&lt;/li&gt;
&lt;li&gt;feedback is expensive to apply&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Fix this small thing”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“This entire approach might be wrong”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now the author has to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rethink everything&lt;/li&gt;
&lt;li&gt;rework large chunks of code&lt;/li&gt;
&lt;li&gt;re-request review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cycle time explodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Merge becomes risky&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bigger the PR, the higher the risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;conflicts with main branch&lt;/li&gt;
&lt;li&gt;outdated assumptions&lt;/li&gt;
&lt;li&gt;broken integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So teams hesitate.&lt;/p&gt;

&lt;p&gt;And hesitation kills flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Work gets batched (and everything slows down)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the core anti-pattern.&lt;/p&gt;

&lt;p&gt;Instead of shipping continuously, developers start batching work:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“I’ll just add one more thing before I open the PR”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Actually, I’ll include this refactor too”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And suddenly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR grows&lt;/li&gt;
&lt;li&gt;review slows&lt;/li&gt;
&lt;li&gt;merge is delayed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s a vicious cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Reason Your PRs Are Too Big
&lt;/h2&gt;

&lt;p&gt;Let’s be honest — this is not a tooling issue.&lt;/p&gt;

&lt;p&gt;It’s behavioral.&lt;/p&gt;

&lt;p&gt;Here are the real causes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. “I want it to be complete”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the most common mindset:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“I’ll open the PR when the feature is done.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sounds reasonable. It’s also completely wrong.&lt;/p&gt;

&lt;p&gt;Because “done” often means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple layers&lt;/li&gt;
&lt;li&gt;edge cases&lt;/li&gt;
&lt;li&gt;polish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which leads to massive PRs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fear of breaking things&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without proper safety mechanisms (tests, feature flags), developers avoid small merges.&lt;/p&gt;

&lt;p&gt;So they wait.&lt;/p&gt;

&lt;p&gt;And wait.&lt;/p&gt;

&lt;p&gt;And accumulate changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Poor slicing of work&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most developers split work like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend PR&lt;/li&gt;
&lt;li&gt;frontend PR&lt;/li&gt;
&lt;li&gt;DB migration PR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vertical slices that deliver value end-to-end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates artificial dependencies and forces larger PRs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Review culture is asynchronous and slow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If reviews take:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hours&lt;/li&gt;
&lt;li&gt;or days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers adapt by batching work.&lt;/p&gt;

&lt;p&gt;Why open a small PR if it will just sit there?&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6-Month Pull Request Problem
&lt;/h2&gt;

&lt;p&gt;Let’s go back to that real example.&lt;/p&gt;

&lt;p&gt;A PR open for six months.&lt;/p&gt;

&lt;p&gt;What does that actually mean?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The branch is completely outdated&lt;/li&gt;
&lt;li&gt;The context is lost&lt;/li&gt;
&lt;li&gt;The author doesn’t remember all decisions&lt;/li&gt;
&lt;li&gt;The reviewers don’t understand it anymore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, you have only two realistic options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Merge it blindly (high risk)&lt;/li&gt;
&lt;li&gt;Close it and start over (lost work)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are bad.&lt;/p&gt;

&lt;p&gt;But the real failure happened much earlier — when the PR was allowed to grow beyond control.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Fix This (Practically)
&lt;/h2&gt;

&lt;p&gt;This is not about theory. These are concrete practices that work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Enforce a hard PR size limit&lt;/strong&gt;&lt;br&gt;
Set a rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Max ~300–400 lines per PR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not as a suggestion.&lt;/p&gt;

&lt;p&gt;As a constraint.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because constraints force better behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;better slicing&lt;/li&gt;
&lt;li&gt;fewer concerns per change&lt;/li&gt;
&lt;li&gt;faster reviews&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Merge incomplete work (safely)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the shift most teams struggle with.&lt;/p&gt;

&lt;p&gt;You don’t need “finished features” to merge.&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;safe code&lt;/li&gt;
&lt;li&gt;controlled exposure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feature flags&lt;/li&gt;
&lt;li&gt;toggles&lt;/li&gt;
&lt;li&gt;hidden UI states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merge early&lt;/li&gt;
&lt;li&gt;iterate safely&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Slice vertically, not horizontally&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR 1: database&lt;/li&gt;
&lt;li&gt;PR 2: API&lt;/li&gt;
&lt;li&gt;PR 3: UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR 1: minimal end-to-end functionality&lt;/li&gt;
&lt;li&gt;PR 2: extend behavior&lt;/li&gt;
&lt;li&gt;PR 3: polish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each PR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delivers something usable&lt;/li&gt;
&lt;li&gt;is independently testable&lt;/li&gt;
&lt;li&gt;is small&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Optimize review speed (not just quality)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set expectations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR should be reviewed within a few hours&lt;/li&gt;
&lt;li&gt;not days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ways to achieve this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;smaller PRs (obviously)&lt;/li&gt;
&lt;li&gt;clear ownership&lt;/li&gt;
&lt;li&gt;synchronous reviews when needed (pairing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Accept “ugly but correct” code (temporarily)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is uncomfortable but critical.&lt;/p&gt;

&lt;p&gt;Sometimes the right move is:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Merge this small, slightly imperfect change now”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Wait until it’s perfectly clean”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because flow matters more than perfection.&lt;/p&gt;

&lt;p&gt;You can always:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;refactor later (but don’t forget — it’s important for code cleanup)&lt;/li&gt;
&lt;li&gt;improve incrementally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Track your actual behavior&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to be serious about this, measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;average PR size&lt;/li&gt;
&lt;li&gt;PR lifetime&lt;/li&gt;
&lt;li&gt;number of merges per day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reality check:&lt;/p&gt;

&lt;p&gt;If your PRs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;live for days&lt;/li&gt;
&lt;li&gt;contain hundreds/thousands of lines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You are not doing trunk-based development.&lt;/p&gt;

&lt;p&gt;No matter what your team says.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Large Architectural Changes?
&lt;/h2&gt;

&lt;p&gt;A common question that comes up here is: does this apply when you’re doing a complete architectural overhaul of a service?&lt;/p&gt;

&lt;p&gt;The short answer is yes — and there’s a specific pattern for it called Branch by Abstraction.&lt;/p&gt;

&lt;p&gt;Instead of keeping the new architecture in a separate Git branch for months (and facing a merge nightmare later), you keep both versions in the main branch simultaneously. It feels messy at first. It’s much safer in practice.&lt;/p&gt;

&lt;p&gt;Here’s how it works in combination with feature flags:&lt;/p&gt;

&lt;p&gt;You merge small, incremental pieces of the new architecture daily — never a big bang.&lt;br&gt;
You run dark launches: the new code executes in production, but its results are ignored or compared against the old implementation. No user impact, real-world validation.&lt;br&gt;
When confidence is high enough, you flip the flag and the new architecture goes live instantly.&lt;br&gt;
The two things that make this work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The flag has to exist from the start. Not added later as an afterthought. If you begin the migration without a flag, you’ll end up in the same situation as a long-lived branch — just in the main branch instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The cleanup phase is not optional. Once the rollout is 100% successful, deleting the old code and removing the flag is not a nice-to-have. It’s part of the work. Skipping this step is how codebases become impossible to understand.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tradeoff is real: having two architectures in the codebase simultaneously requires more discipline and coordination from the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Mindset Shift
&lt;/h2&gt;

&lt;p&gt;This is the uncomfortable truth:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Small pull requests are not a technical practice.&lt;br&gt;
They are a discipline.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;They require you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;let go of “completeness”&lt;/li&gt;
&lt;li&gt;embrace incremental delivery&lt;/li&gt;
&lt;li&gt;prioritize flow over polish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that’s hard.&lt;/p&gt;

&lt;p&gt;Because it goes against how most developers naturally think about work.&lt;/p&gt;

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

&lt;p&gt;That 6-month pull request wasn’t an exception.&lt;/p&gt;

&lt;p&gt;It was just an extreme version of a very common problem.&lt;/p&gt;

&lt;p&gt;Most teams are operating with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;oversized changes&lt;/li&gt;
&lt;li&gt;slow feedback&lt;/li&gt;
&lt;li&gt;delayed merges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then they wonder why delivery feels slow.&lt;/p&gt;

&lt;p&gt;If you fix just one thing, fix this:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Make your pull requests smaller. Radically smaller.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everything else — faster reviews, fewer bugs, smoother delivery — follows from that.&lt;/p&gt;

&lt;p&gt;Not the other way around.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>devops</category>
      <category>software</category>
    </item>
    <item>
      <title>Laravel Testing Mistakes That Make Your Tests Useless</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 21 Apr 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/laravel-testing-mistakes-that-make-your-tests-useless-136h</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/laravel-testing-mistakes-that-make-your-tests-useless-136h</guid>
      <description>&lt;p&gt;Testing in Laravel can feel straightforward at first. You write a few tests, run &lt;code&gt;php artisan test&lt;/code&gt;, see green output, and move on. But here’s the uncomfortable truth: &lt;strong&gt;many passing tests don’t actually protect your application&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your tests don’t catch real bugs, they’re not just useless—they give you a false sense of confidence.&lt;/p&gt;

&lt;p&gt;In this article, we’ll go through the most common Laravel testing mistakes that quietly break the value of your test suite, along with practical examples and better approaches.&lt;/p&gt;

&lt;p&gt;You can be also interested in testing databse logic: &lt;a href="https://codecraftdiary.com/2026/01/03/testing-database-logic-what-to-test-what-to-skip-and-why-it-matters/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/01/03/testing-database-logic-what-to-test-what-to-skip-and-why-it-matters/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Testing Implementation Instead of Behavior
&lt;/h2&gt;

&lt;p&gt;One of the biggest mistakes is writing tests that mirror your code instead of validating what your application actually does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bad example
&lt;/h3&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_it_calls_service_method&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mockery&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserService&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="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;shouldReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'createUser'&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;once&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$controller&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;UserController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$controller&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;store&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;Request&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test only checks that a method was called. It doesn’t verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what was created&lt;/li&gt;
&lt;li&gt;whether the data is correct&lt;/li&gt;
&lt;li&gt;whether anything actually works&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Better approach
&lt;/h3&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_is_created&lt;/span&gt;&lt;span class="p"&gt;()&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;post&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;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'John Doe'&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;'john@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&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;201&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;'john@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;Focus on &lt;strong&gt;observable behavior&lt;/strong&gt;, not internal calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Overusing Mocks
&lt;/h2&gt;

&lt;p&gt;Mocks are powerful—but overusing them leads to fragile and meaningless tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;When everything is mocked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you’re not testing real integration&lt;/li&gt;
&lt;li&gt;your tests pass even if the system is broken
&lt;/li&gt;
&lt;/ul&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="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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/weather'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you nothing about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;response structure&lt;/li&gt;
&lt;li&gt;data correctness&lt;/li&gt;
&lt;li&gt;edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Better approach
&lt;/h3&gt;

&lt;p&gt;Mock only what you must (external services), and assert meaningful output:&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;'temp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&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;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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/weather'&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;assertJson&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'temperature'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&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;Rule of thumb:&lt;br&gt;
&lt;strong&gt;Mock boundaries, not your own logic.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Writing Tests That Always Pass
&lt;/h2&gt;

&lt;p&gt;Some tests are written in a way that they can’t fail—even if the code is broken.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example
&lt;/h3&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_response_is_ok&lt;/span&gt;&lt;span class="p"&gt;()&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;get&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="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This test will pass even if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the response is empty&lt;/li&gt;
&lt;li&gt;the wrong data is returned&lt;/li&gt;
&lt;li&gt;business logic is broken&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Better approach
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&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;assertJsonStructure&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&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;Or even better:&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;$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;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask yourself:&lt;br&gt;
&lt;strong&gt;“What bug would this test catch?”&lt;/strong&gt;&lt;br&gt;
If the answer is “none”, rewrite it.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Ignoring Edge Cases
&lt;/h2&gt;

&lt;p&gt;Most bugs don’t happen in the “happy path”. They happen at the edges.&lt;/p&gt;
&lt;h3&gt;
  
  
  Common mistake
&lt;/h3&gt;

&lt;p&gt;Only testing valid input:&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;$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;'/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;'john@example.com'&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;h3&gt;
  
  
  Better approach
&lt;/h3&gt;

&lt;p&gt;Test invalid scenarios:&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;$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;'/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;'not-an-email'&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;assertSessionHasErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing fields&lt;/li&gt;
&lt;li&gt;duplicate values&lt;/li&gt;
&lt;li&gt;unexpected input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good tests try to &lt;strong&gt;break your application&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Testing Too Much in Unit Tests
&lt;/h2&gt;

&lt;p&gt;Unit tests should be fast and focused. But many developers turn them into mini integration tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&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_creation&lt;/span&gt;&lt;span class="p"&gt;()&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;=&lt;/span&gt; &lt;span class="nc"&gt;OrderService&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="mf"&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;assertDatabaseHas&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="p"&gt;[&lt;/span&gt;&lt;span class="mf"&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 mixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;business logic&lt;/li&gt;
&lt;li&gt;database layer&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Better approach
&lt;/h3&gt;

&lt;p&gt;Split responsibilities:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unit test (logic only):&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_total_price_is_calculated_correctly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderCalculator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;calculate&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="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;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$total&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;Feature test (full flow):&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="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;'/orders'&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="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;'orders'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep your test layers clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Not Using Factories Properly
&lt;/h2&gt;

&lt;p&gt;Laravel factories are powerful, but many developers misuse them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Hardcoding everything:&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;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="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Test'&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;'test@example.com'&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;h3&gt;
  
  
  Better approach
&lt;/h3&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even better:&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;state&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;less boilerplate&lt;/li&gt;
&lt;li&gt;more flexible tests&lt;/li&gt;
&lt;li&gt;easier maintenance&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Not Cleaning Up Test Data
&lt;/h2&gt;

&lt;p&gt;Dirty test data can cause flaky tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Tests depend on previous state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;Use:&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;Illuminate\Foundation\Testing\RefreshDatabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clean DB for each test&lt;/li&gt;
&lt;li&gt;consistent results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flaky tests destroy trust in your test suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Writing Tests That Are Too Complex
&lt;/h2&gt;

&lt;p&gt;If your test is hard to read, it’s probably doing too much.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&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_everything&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 50 lines of setup&lt;/span&gt;
    &lt;span class="c1"&gt;// 10 assertions&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Better approach
&lt;/h3&gt;

&lt;p&gt;Break it down:&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_register&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;test_email_must_be_unique&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;test_password_is_required&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;Each test should answer one question.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Ignoring Performance
&lt;/h2&gt;

&lt;p&gt;Slow tests are often skipped—and skipped tests are useless tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;too many DB calls&lt;/li&gt;
&lt;li&gt;unnecessary setup&lt;/li&gt;
&lt;li&gt;heavy fixtures&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tips
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;use in-memory database (SQLite)&lt;/li&gt;
&lt;li&gt;avoid unnecessary seeding&lt;/li&gt;
&lt;li&gt;keep unit tests fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fast tests = tests you actually run.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Not Testing Real User Flows
&lt;/h2&gt;

&lt;p&gt;Testing isolated pieces is not enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;You test services and controllers separately, but never the full flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&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_register_and_login&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/register'&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;'john@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'password'&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/login'&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;'john@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'password'&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;assertRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/dashboard'&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 what actually matters:&lt;br&gt;
&lt;strong&gt;Can the user complete the action?&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;Laravel makes testing easy—but writing &lt;em&gt;useful&lt;/em&gt; tests is a different skill.&lt;/p&gt;

&lt;p&gt;If your tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only check status codes&lt;/li&gt;
&lt;li&gt;mock everything&lt;/li&gt;
&lt;li&gt;mirror your implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…then they’re not protecting your application.&lt;/p&gt;

&lt;p&gt;Instead, focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;real behavior&lt;/li&gt;
&lt;li&gt;meaningful assertions&lt;/li&gt;
&lt;li&gt;edge cases&lt;/li&gt;
&lt;li&gt;realistic user flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A smaller set of high-quality tests is far more valuable than a large suite of weak ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Checklist
&lt;/h2&gt;

&lt;p&gt;Before committing a test, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this test fail if something important breaks?&lt;/li&gt;
&lt;li&gt;Am I testing behavior, not implementation?&lt;/li&gt;
&lt;li&gt;Would this catch a real bug?&lt;/li&gt;
&lt;li&gt;Is this test simple and readable?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is “no”, it’s time to improve it.&lt;/p&gt;




&lt;p&gt;Well-written tests are not just about coverage—they’re about confidence. And confidence comes from tests that actually matter.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>Fat Controller to Clean Architecture in Laravel (Step-by-Step Refactor)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 14 Apr 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/fat-controller-to-clean-architecture-in-laravel-step-by-step-refactor-3eii</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/fat-controller-to-clean-architecture-in-laravel-step-by-step-refactor-3eii</guid>
      <description>&lt;p&gt;Refactoring a fat controller in Laravel is one of the most impactful improvements you can make in a growing codebase. As projects evolve, controllers often become overloaded with validation, business logic, and side effects, making them difficult to maintain and test.&lt;/p&gt;

&lt;p&gt;A controller starts small. Clean. Readable.&lt;/p&gt;

&lt;p&gt;Then features get added.&lt;/p&gt;

&lt;p&gt;Deadlines hit.&lt;/p&gt;

&lt;p&gt;Logic piles up.&lt;/p&gt;

&lt;p&gt;And suddenly, you’re staring at a 500-line controller that handles validation, business logic, database writes, API calls, and maybe even a bit of formatting “just for now.”&lt;/p&gt;

&lt;p&gt;This is what we call a &lt;strong&gt;Fat Controller&lt;/strong&gt; — and it’s one of the most common maintainability problems in Laravel applications.&lt;/p&gt;

&lt;p&gt;In this article, we’ll take a real-world approach and refactor a fat controller into a cleaner, scalable structure using principles inspired by Clean Architecture.&lt;/p&gt;

&lt;p&gt;No theory overload. Just practical steps.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Problem: A Real Fat Controller Example
&lt;/h1&gt;

&lt;p&gt;Let’s start with something painfully familiar:&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;OrderController&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;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="c1"&gt;// Validation&lt;/span&gt;
        &lt;span class="nv"&gt;$validated&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;validate&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="s1"&gt;'required|exists:users,id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="c1"&gt;// Business logic&lt;/span&gt;
        &lt;span class="nv"&gt;$total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;$validated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&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;$product&lt;/span&gt;&lt;span class="p"&gt;)&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;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Product not found'&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;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Not enough stock'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'quantity'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

            &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'quantity'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Save order&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;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="nv"&gt;$validated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'total'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="c1"&gt;// Save items&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;$validated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;OrderItem&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;'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;'product_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'quantity'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'quantity'&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="c1"&gt;// External API call&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://example.com/webhook'&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="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="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What’s wrong here?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Controller handles &lt;strong&gt;too many responsibilities&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Business logic is &lt;strong&gt;not reusable&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hard to &lt;strong&gt;test&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tight coupling to Eloquent and external APIs&lt;/li&gt;
&lt;li&gt;Changes are risky&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Goal: What “Clean” Looks Like
&lt;/h1&gt;

&lt;p&gt;We want to move toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thin controllers&lt;/li&gt;
&lt;li&gt;Isolated business logic&lt;/li&gt;
&lt;li&gt;Testable services&lt;/li&gt;
&lt;li&gt;Clear boundaries between layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’re not going full academic Clean Architecture. We’re applying &lt;strong&gt;just enough structure to stay sane&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Step 1: Extract Business Logic into a Service
&lt;/h1&gt;

&lt;p&gt;First, move the core logic out of the controller.&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;CreateOrderService&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;array&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Order&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&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;$product&lt;/span&gt;&lt;span class="p"&gt;)&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;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Product not found'&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;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Not enough stock'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'quantity'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

            &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'quantity'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&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;=&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;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="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'total'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="p"&gt;,&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;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;OrderItem&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;'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;'product_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'quantity'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'quantity'&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="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&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;'https://example.com/webhook'&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="p"&gt;]);&lt;/span&gt;

        &lt;span class="k"&gt;return&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Controller becomes:&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;OrderController&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;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="kt"&gt;CreateOrderService&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$validated&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;validate&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="s1"&gt;'required|exists:users,id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|array'&lt;/span&gt;&lt;span class="p"&gt;,&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;=&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$validated&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="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Improvement:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Controller is now thin&lt;/li&gt;
&lt;li&gt;Logic is reusable&lt;/li&gt;
&lt;li&gt;Easier to test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But we’re not done yet.&lt;/p&gt;




&lt;h1&gt;
  
  
  Step 2: Introduce a Data Transfer Object (DTO)
&lt;/h1&gt;

&lt;p&gt;Passing raw arrays is fragile.&lt;/p&gt;

&lt;p&gt;Let’s fix 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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderData&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;int&lt;/span&gt; &lt;span class="nv"&gt;$userId&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="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;fromArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;self&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;new&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&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;Update controller:&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;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$validated&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;=&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update 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="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;CreateOrderData&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Improvement:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Stronger typing&lt;/li&gt;
&lt;li&gt;Safer refactoring&lt;/li&gt;
&lt;li&gt;Clear contract&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Step 3: Decouple External Dependencies
&lt;/h1&gt;

&lt;p&gt;Right now, your service is tightly coupled to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Eloquent models&lt;/li&gt;
&lt;li&gt;HTTP client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s extract the webhook logic.&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;OrderWebhookService&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;send&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="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&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;'https://example.com/webhook'&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="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;Inject 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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderService&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;OrderWebhookService&lt;/span&gt; &lt;span class="nv"&gt;$webhook&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;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;CreateOrderData&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Order&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// logic...&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;webhook&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="nv"&gt;$order&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;$order&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;h3&gt;
  
  
  Improvement:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;External side effects are isolated&lt;/li&gt;
&lt;li&gt;Easier to mock in tests&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Step 4: Make It Testable
&lt;/h1&gt;

&lt;p&gt;Now you can test the service independently:&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_it_creates_order&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateOrderService&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="nv"&gt;$data&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;CreateOrderData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'quantity'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&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;assertNotNull&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;id&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;Before refactoring, this would require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP mocking&lt;/li&gt;
&lt;li&gt;Controller testing&lt;/li&gt;
&lt;li&gt;Complex setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now it’s isolated.&lt;/p&gt;




&lt;h1&gt;
  
  
  Step 5: Optional — Introduce Repositories (Only If Needed)
&lt;/h1&gt;

&lt;p&gt;Don’t overengineer this.&lt;/p&gt;

&lt;p&gt;But if your app grows, you might extract:&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;ProductRepository&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;find&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;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?Product&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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;Then inject it into the service.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to do this:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Multiple data sources&lt;/li&gt;
&lt;li&gt;Complex queries&lt;/li&gt;
&lt;li&gt;Domain logic reuse&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When NOT to:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Simple CRUD apps&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Before vs After
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Controller size&lt;/td&gt;
&lt;td&gt;Huge&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusability&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coupling&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Reduced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainability&lt;/td&gt;
&lt;td&gt;Painful&lt;/td&gt;
&lt;td&gt;Scalable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  Common Mistakes to Avoid
&lt;/h1&gt;

&lt;h3&gt;
  
  
  1. Moving everything blindly into services
&lt;/h3&gt;

&lt;p&gt;You’ll just create &lt;strong&gt;fat services instead of fat controllers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Keep services focused.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Overengineering with too many layers
&lt;/h3&gt;

&lt;p&gt;You don’t need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 interfaces&lt;/li&gt;
&lt;li&gt;5 abstractions&lt;/li&gt;
&lt;li&gt;enterprise architecture™&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start simple. Evolve when needed.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Ignoring boundaries
&lt;/h3&gt;

&lt;p&gt;Controllers = HTTP&lt;br&gt;
Services = business logic&lt;br&gt;
Models = persistence&lt;/p&gt;

&lt;p&gt;Mixing these again = back to chaos.&lt;/p&gt;




&lt;h1&gt;
  
  
  Key Takeaways
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Fat controllers are a &lt;strong&gt;symptom&lt;/strong&gt;, not the root problem&lt;/li&gt;
&lt;li&gt;The real issue is &lt;strong&gt;mixed responsibilities&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Start with &lt;strong&gt;service extraction&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add DTOs for safety&lt;/li&gt;
&lt;li&gt;Isolate side effects (APIs, events)&lt;/li&gt;
&lt;li&gt;Only introduce more abstraction when justified&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Final Thought
&lt;/h1&gt;

&lt;p&gt;Clean Architecture in Laravel doesn’t mean rewriting your app into a textbook diagram.&lt;/p&gt;

&lt;p&gt;It means one thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Making your code easier to change without fear.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the fastest way to get there?&lt;/p&gt;

&lt;p&gt;Start killing your fat controllers — one method at a time.&lt;/p&gt;




&lt;p&gt;If this is something you’re dealing with right now, your next step is simple:&lt;/p&gt;

&lt;p&gt;Pick your worst controller and extract just one action into a service.&lt;/p&gt;

&lt;p&gt;That’s how clean architecture actually starts.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>laravel</category>
      <category>refactorit</category>
      <category>development</category>
    </item>
    <item>
      <title>Trunk-Based Development: Why Most Teams Think They Use It (But Don’t)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 07 Apr 2026 13:20:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/trunk-based-development-why-most-teams-think-they-use-it-but-dont-o4g</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/trunk-based-development-why-most-teams-think-they-use-it-but-dont-o4g</guid>
      <description>&lt;p&gt;Trunk-Based Development sounds simple.&lt;/p&gt;

&lt;p&gt;No long-lived branches.&lt;br&gt;
Frequent merges.&lt;br&gt;
Small, incremental changes.&lt;/p&gt;

&lt;p&gt;Most teams will tell you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Yeah, we basically do trunk-based development.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In practice, they don’t.&lt;/p&gt;

&lt;p&gt;What they usually have is a hybrid that keeps the downsides of feature branches — while pretending to get the benefits of trunk-based development.&lt;/p&gt;

&lt;p&gt;I’ve seen this pattern in multiple backend teams. On paper, everything looks modern. In reality, delivery is still slow, pull requests are large, and integration is painful.&lt;/p&gt;

&lt;p&gt;So let’s break down what’s actually going on.&lt;/p&gt;


&lt;h1&gt;
  
  
  The Illusion of Trunk-Based Development
&lt;/h1&gt;

&lt;p&gt;Ask a team how they work, and you’ll often hear something like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“We merge to main frequently”&lt;/li&gt;
&lt;li&gt;“We don’t keep branches for too long”&lt;/li&gt;
&lt;li&gt;“We try to keep PRs small”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds good.&lt;/p&gt;

&lt;p&gt;But then you look closer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PRs are open for 3–5 days&lt;/li&gt;
&lt;li&gt;branches still contain multiple features&lt;/li&gt;
&lt;li&gt;merges are delayed because reviews are slow&lt;/li&gt;
&lt;li&gt;developers are afraid to merge unfinished work&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This is just &lt;strong&gt;shorter feature branches&lt;/strong&gt;.&lt;/p&gt;


&lt;h1&gt;
  
  
  What Real Trunk-Based Development Actually Requires
&lt;/h1&gt;

&lt;p&gt;Trunk-based development is not about branches.&lt;/p&gt;

&lt;p&gt;It’s about &lt;strong&gt;integration frequency and safety&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At its core, it requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merging to main at least daily (ideally multiple times per day)&lt;/li&gt;
&lt;li&gt;keeping changes small enough to review quickly&lt;/li&gt;
&lt;li&gt;having strong safety mechanisms in place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without these, trunk-based development collapses.&lt;/p&gt;


&lt;h1&gt;
  
  
  Where Most Teams Break
&lt;/h1&gt;
&lt;h2&gt;
  
  
  1. Pull Requests Are Still Too Big
&lt;/h2&gt;

&lt;p&gt;This is the biggest issue.&lt;/p&gt;

&lt;p&gt;A developer starts a “small” feature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adds endpoint&lt;/li&gt;
&lt;li&gt;updates service&lt;/li&gt;
&lt;li&gt;modifies database&lt;/li&gt;
&lt;li&gt;adds validation&lt;/li&gt;
&lt;li&gt;fixes something unrelated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the PR has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 files changed&lt;/li&gt;
&lt;li&gt;600+ lines&lt;/li&gt;
&lt;li&gt;multiple concerns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Review slows down. Feedback increases. Merge is delayed.&lt;/p&gt;

&lt;p&gt;At that point, it doesn’t matter what you call your workflow —&lt;br&gt;
you’re not doing trunk-based development.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Code Reviews Block Integration
&lt;/h2&gt;

&lt;p&gt;In theory:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We merge frequently”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In reality:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We merge when the PR gets approved”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That delay is critical.&lt;/p&gt;

&lt;p&gt;If reviews take:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 day → integration is delayed&lt;/li&gt;
&lt;li&gt;2–3 days → conflicts increase&lt;/li&gt;
&lt;li&gt;5 days → context is lost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now developers start stacking changes on top of each other.&lt;/p&gt;

&lt;p&gt;And suddenly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;branches live longer&lt;/li&gt;
&lt;li&gt;PRs get bigger&lt;/li&gt;
&lt;li&gt;merges become risky&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  3. Teams Are Afraid to Merge Incomplete Work
&lt;/h2&gt;

&lt;p&gt;This is subtle but important.&lt;/p&gt;

&lt;p&gt;Developers often think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I can’t merge this yet — it’s not finished”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So they keep working on the branch.&lt;/p&gt;

&lt;p&gt;The problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the longer the branch lives&lt;/li&gt;
&lt;li&gt;the more it diverges&lt;/li&gt;
&lt;li&gt;the harder it is to merge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trunk-based development requires a different mindset:&lt;/p&gt;

&lt;p&gt;You merge incomplete work safely.&lt;/p&gt;


&lt;h1&gt;
  
  
  The Missing Piece: Feature Flags
&lt;/h1&gt;

&lt;p&gt;Most teams skip this.&lt;/p&gt;

&lt;p&gt;And without it, trunk-based development doesn’t work.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;

&lt;p&gt;You’re building a new payment flow.&lt;/p&gt;

&lt;p&gt;Without feature flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you need to finish everything before merging&lt;/li&gt;
&lt;li&gt;you keep a long-lived branch&lt;/li&gt;
&lt;li&gt;integration is delayed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With feature flags:&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Feature&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'new_payment_flow'&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;newFlow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;oldFlow&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 you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merge partial work&lt;/li&gt;
&lt;li&gt;deploy continuously&lt;/li&gt;
&lt;li&gt;control exposure&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Real Example From Practice
&lt;/h1&gt;

&lt;p&gt;I worked with a team that claimed they were doing trunk-based development.&lt;/p&gt;

&lt;p&gt;Metrics looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;average PR size: ~500 lines&lt;/li&gt;
&lt;li&gt;review time: 2–3 days&lt;/li&gt;
&lt;li&gt;merges per developer: ~2 per week&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After digging in, the issue was clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developers grouped work “to be efficient”&lt;/li&gt;
&lt;li&gt;reviews were asynchronous and slow&lt;/li&gt;
&lt;li&gt;no feature flags&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  What changed
&lt;/h3&gt;

&lt;p&gt;We introduced 3 rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PR must be mergeable within the same day&lt;/li&gt;
&lt;li&gt;no PR over ~300 lines (soft limit)&lt;/li&gt;
&lt;li&gt;feature flags for incomplete features&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Result after ~3 weeks:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PR size dropped by ~40%&lt;/li&gt;
&lt;li&gt;review time dropped to hours&lt;/li&gt;
&lt;li&gt;merges increased to multiple per day&lt;/li&gt;
&lt;li&gt;production issues decreased&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not because developers got better.&lt;/p&gt;

&lt;p&gt;Because the system changed.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Hidden Constraint: CI/CD Speed
&lt;/h1&gt;

&lt;p&gt;Here’s something teams often ignore:&lt;/p&gt;

&lt;p&gt;You cannot do trunk-based development with slow pipelines.&lt;/p&gt;

&lt;p&gt;If your CI takes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20 minutes → friction&lt;/li&gt;
&lt;li&gt;40 minutes → developers wait&lt;/li&gt;
&lt;li&gt;60+ minutes → people stop merging frequently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So what happens?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developers batch changes&lt;/li&gt;
&lt;li&gt;PRs get bigger&lt;/li&gt;
&lt;li&gt;integration slows down&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Rule of thumb (2026 reality):
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;CI under 10 minutes → good&lt;/li&gt;
&lt;li&gt;under 5 minutes → ideal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything above that:&lt;br&gt;
→ you’re actively fighting your workflow&lt;/p&gt;




&lt;h1&gt;
  
  
  Why This Matters More in 2026
&lt;/h1&gt;

&lt;p&gt;With AI-assisted coding, developers can generate code faster than ever.&lt;/p&gt;

&lt;p&gt;That creates a new problem:&lt;/p&gt;

&lt;p&gt;volume of changes increases&lt;br&gt;
but review capacity doesn’t&lt;/p&gt;

&lt;p&gt;If you don’t enforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;small changes&lt;/li&gt;
&lt;li&gt;fast integration&lt;/li&gt;
&lt;li&gt;clear boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;your workflow collapses under its own weight.&lt;/p&gt;




&lt;h1&gt;
  
  
  How to Tell If You’re Actually Doing It
&lt;/h1&gt;

&lt;p&gt;Be honest and check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you merge to main multiple times per day?&lt;/li&gt;
&lt;li&gt;Are most PRs reviewed within hours, not days?&lt;/li&gt;
&lt;li&gt;Can you safely merge incomplete work?&lt;/li&gt;
&lt;li&gt;Are branches short-lived (hours, not days)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If not:&lt;br&gt;
→ you’re not doing trunk-based development&lt;/p&gt;




&lt;h1&gt;
  
  
  Practical Rules You Can Apply Tomorrow
&lt;/h1&gt;

&lt;p&gt;If you want to move closer to real trunk-based development, start here:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Limit PR size
&lt;/h3&gt;

&lt;p&gt;Not as a guideline — as a rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Optimize for review speed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;fewer changes&lt;/li&gt;
&lt;li&gt;clearer intent&lt;/li&gt;
&lt;li&gt;less context switching&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Introduce feature flags
&lt;/h3&gt;

&lt;p&gt;Without them, you’re stuck.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Fix your CI/CD bottlenecks
&lt;/h3&gt;

&lt;p&gt;Speed is not a luxury — it’s a requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Stop batching work
&lt;/h3&gt;

&lt;p&gt;Batching feels efficient.&lt;br&gt;
It’s not.&lt;/p&gt;




&lt;h1&gt;
  
  
  Closing Thought
&lt;/h1&gt;

&lt;p&gt;Trunk-based development is not a branching strategy.&lt;/p&gt;

&lt;p&gt;It’s a discipline.&lt;/p&gt;

&lt;p&gt;Most teams don’t fail because they don’t understand it.&lt;br&gt;
They fail because they don’t change the constraints that make it possible.&lt;/p&gt;

&lt;p&gt;If your:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PRs are big&lt;/li&gt;
&lt;li&gt;reviews are slow&lt;/li&gt;
&lt;li&gt;CI is slow&lt;/li&gt;
&lt;li&gt;merges are risky&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then it doesn’t matter what you call your workflow.&lt;/p&gt;

&lt;p&gt;You’re still doing feature-branch development — just with better branding.&lt;/p&gt;

&lt;p&gt;And that’s exactly why your delivery still feels slower than it should.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>devops</category>
      <category>software</category>
      <category>development</category>
    </item>
  </channel>
</rss>
