<?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.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>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>
    <item>
      <title>Laravel Testing: RefreshDatabase vs DatabaseTransactions (What Actually Matters)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 31 Mar 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/laravel-testing-refreshdatabase-vs-databasetransactions-what-actually-matters-4fje</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/laravel-testing-refreshdatabase-vs-databasetransactions-what-actually-matters-4fje</guid>
      <description>&lt;p&gt;Laravel RefreshDatabase vs DatabaseTransactions is one of the most common sources of confusion when writing tests in Laravel.&lt;/p&gt;

&lt;p&gt;Choosing the wrong approach can lead to flaky tests, hidden bugs, and unreliable results.&lt;/p&gt;

&lt;p&gt;When writing tests in Laravel, database state can quickly become a source of confusion.&lt;/p&gt;

&lt;p&gt;One test passes, another fails. Data seems to “leak” between tests. Records appear when they shouldn’t — or disappear when you expect them to exist.&lt;/p&gt;

&lt;p&gt;If you’ve experienced this, you’re not alone.&lt;/p&gt;

&lt;p&gt;In most cases, the issue comes down to how your tests handle the database. Laravel provides two primary approaches for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RefreshDatabase
&lt;/li&gt;
&lt;li&gt;DatabaseTransactions
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first glance, they seem similar. In reality, they behave very differently — and choosing the wrong one can lead to subtle bugs or false confidence in your test suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding the Core Problem
&lt;/h2&gt;

&lt;p&gt;Tests must be isolated.&lt;/p&gt;

&lt;p&gt;Each test should run independently, without being affected by previous tests.&lt;/p&gt;

&lt;p&gt;If your database is not reset properly between tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;data can persist unexpectedly
&lt;/li&gt;
&lt;li&gt;test results become unreliable
&lt;/li&gt;
&lt;li&gt;debugging becomes painful
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Laravel solves this problem using two strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reset the database completely
&lt;/li&gt;
&lt;li&gt;Wrap each test in a transaction and roll it back
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Option 1: RefreshDatabase
&lt;/h2&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;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserTest&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;RefreshDatabase&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;How it works&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs migrations before tests&lt;/li&gt;
&lt;li&gt;Ensures a clean database state&lt;/li&gt;
&lt;li&gt;Uses transactions internally when possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Advantages&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High reliability&lt;/li&gt;
&lt;li&gt;Works with queues, jobs, events&lt;/li&gt;
&lt;li&gt;Predictable behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slower&lt;/li&gt;
&lt;li&gt;Heavier setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Option 2: DatabaseTransactions
&lt;/h2&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\DatabaseTransactions&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;UserTest&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;DatabaseTransactions&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;How it works&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts a database transaction before each test&lt;/li&gt;
&lt;li&gt;Rolls it back after the test finishes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Advantages&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast&lt;/li&gt;
&lt;li&gt;Lightweight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages&lt;/strong&gt;&lt;br&gt;
Does NOT work well with queues or async processes&lt;br&gt;
Can lead to hidden inconsistencies&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Each
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;testing queues or jobs&lt;/li&gt;
&lt;li&gt;working with multiple DB connections&lt;/li&gt;
&lt;li&gt;you need maximum reliability&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;testing simple DB logic&lt;/li&gt;
&lt;li&gt;performance is critical&lt;/li&gt;
&lt;li&gt;everything runs in one request lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Things Break in Real Projects
&lt;/h2&gt;

&lt;p&gt;This is where most developers run into problems.&lt;/p&gt;

&lt;p&gt;Especially when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;jobs don’t see database data&lt;/li&gt;
&lt;li&gt;tests pass locally but fail in CI&lt;/li&gt;
&lt;li&gt;retries cause unexpected behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;-&amp;gt;&lt;/strong&gt; I go deeper into real-world failures, debugging strategies, and edge cases here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-&amp;gt;&lt;/strong&gt; &lt;a href="https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/03/28/laravel-refreshdatabase-vs-databasetransactions/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Use RefreshDatabase for reliability&lt;br&gt;
Use DatabaseTransactions for speed&lt;br&gt;
Avoid mixing both&lt;br&gt;
Be careful with queues and async behavior&lt;/p&gt;

&lt;p&gt;If you're working with Laravel testing, you might also find useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue testing pitfalls&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;-&amp;gt;&lt;/strong&gt; &lt;a href="https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/03/08/laravel-queue-testing-jobs-retries/&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Feature testing in PHP&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;-&amp;gt;&lt;/strong&gt; &lt;a href="https://codecraftdiary.com/2025/10/30/feature-testing-in-php-ensuring-the-whole-system-works-together/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2025/10/30/feature-testing-in-php-ensuring-the-whole-system-works-together/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>laravel</category>
      <category>php</category>
      <category>testing</category>
    </item>
    <item>
      <title>AI-Driven Refactoring in PHP: When to Trust Copilot (and When to Take the Wheel)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 24 Mar 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/ai-driven-refactoring-in-php-when-to-trust-copilot-and-when-to-take-the-wheel-405b</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/ai-driven-refactoring-in-php-when-to-trust-copilot-and-when-to-take-the-wheel-405b</guid>
      <description>&lt;p&gt;In 2026, the biggest performance gains in PHP don’t come from micro-optimizations—they come from I/O strategy.&lt;/p&gt;

&lt;p&gt;Legacy PHP codebases (5–10 years old) are overwhelmingly synchronous and blocking. Refactoring them toward concurrency—using Fibers with an event loop like Revolt, Amp, or ReactPHP—can unlock massive gains.&lt;/p&gt;

&lt;p&gt;But this is also where AI tools like Copilot become dangerous.&lt;/p&gt;

&lt;p&gt;They can accelerate the refactor—or quietly corrupt your architecture.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/02/28/singleton-pattern-in-php/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Prompt–Refactor–Verify” Cycle
&lt;/h2&gt;

&lt;p&gt;When working with AI, the difference between success and subtle bugs is prompt precision.&lt;/p&gt;

&lt;p&gt;Bad prompt:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Make this async.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Good prompt:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Identify I/O-bound operations. Refactor using Fibers with an event loop (Revolt/Amp). Avoid shared mutable state. Ensure deterministic completion.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you paste a 500-line method without context, AI will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;miss hidden dependencies&lt;/li&gt;
&lt;li&gt;ignore state coupling&lt;/li&gt;
&lt;li&gt;introduce non-obvious bugs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI is a transformer, not a system thinker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example: Legacy Image Processor
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (typical legacy 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="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$image&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;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// blocking&lt;/span&gt;
    &lt;span class="nv"&gt;$resized&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;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// CPU&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;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$resized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// blocking&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. Predictable. Slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After (controlled concurrency with Amp)&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Amp\Future&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;Amp\async&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$tasks&lt;/span&gt; &lt;span class="o"&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;$urls&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$tasks&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$image&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;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// non-blocking if using async client&lt;/span&gt;
        &lt;span class="nv"&gt;$resized&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;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$image&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="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$resized&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;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Future\awaitAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tasks&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;What changed?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I/O is now concurrent&lt;/li&gt;
&lt;li&gt;Execution is interleaved, not parallel threads&lt;/li&gt;
&lt;li&gt;The bottleneck shifts from waiting → throughput&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The AI Trap: “It Works” ≠ “It’s Safe”
&lt;/h2&gt;

&lt;p&gt;AI often produces code that looks correct and even passes tests.&lt;/p&gt;

&lt;p&gt;But there are hidden risks.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Shared State Issues&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is dangerous:&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="n"&gt;processedUrls&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Not because PHP is multithreaded—but because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;execution is interleaved&lt;/li&gt;
&lt;li&gt;order is no longer deterministic&lt;/li&gt;
&lt;li&gt;side effects accumulate unpredictably&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;return values → aggregate later&lt;/li&gt;
&lt;li&gt;avoid mutation inside async tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. The “AI Echo Chamber”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the most dangerous patterns:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI writes a bug → AI writes a test → test passes&lt;/em&gt;&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="c1"&gt;// Bug: returns null on failure&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI-generated test:&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;expect&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;process&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;toBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is green. Everything is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. “Happy Path” Bias&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI prefers:&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;try&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;In production systems, this is data loss.&lt;/p&gt;

&lt;p&gt;Senior-level expectation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit exceptions&lt;/li&gt;
&lt;li&gt;domain-level error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preventing Architecture Drift
&lt;/h2&gt;

&lt;p&gt;The biggest long-term risk isn’t bugs.&lt;/p&gt;

&lt;p&gt;It’s &lt;strong&gt;inconsistency&lt;/strong&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Class A on Monday&lt;/li&gt;
&lt;li&gt;Class B on Tuesday&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;patterns diverge&lt;/li&gt;
&lt;li&gt;abstractions don’t align&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Because AI only sees &lt;strong&gt;the current file&lt;/strong&gt;, not your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Constraint Header” Strategy
&lt;/h2&gt;

&lt;p&gt;Before any refactor, define rules:&lt;/p&gt;

&lt;p&gt;Context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strict types only&lt;/li&gt;
&lt;li&gt;No direct DB calls (Repository pattern)&lt;/li&gt;
&lt;li&gt;Async only via Amp&lt;/li&gt;
&lt;li&gt;Immutable DTOs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Task:&lt;br&gt;
Refactor InvoiceGenerator&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;random design decisions&lt;/li&gt;
&lt;li&gt;pattern drift&lt;/li&gt;
&lt;li&gt;“AI creativity” in critical code&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Killing the “God Constructor”
&lt;/h2&gt;

&lt;p&gt;Legacy PHP often looks like 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;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;Logger&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;Mailer&lt;/span&gt; &lt;span class="nv"&gt;$mailer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;Cache&lt;/span&gt; &lt;span class="nv"&gt;$cache&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="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;AI’s default move:&lt;br&gt;
→ add another dependency&lt;/p&gt;

&lt;p&gt;Wrong direction.&lt;/p&gt;
&lt;h2&gt;
  
  
  Refactoring toward composition
&lt;/h2&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderProcessor&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;Notifier&lt;/span&gt; &lt;span class="nv"&gt;$notifier&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;process&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;match&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;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;Status&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;$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;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Status&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AlreadyPaidException&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="k"&gt;default&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;\UnhandledMatchError&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;notifier&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Order &lt;/span&gt;&lt;span class="si"&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="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; processed"&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;Key idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fewer responsibilities per class&lt;/li&gt;
&lt;li&gt;clearer boundaries&lt;/li&gt;
&lt;li&gt;easier testing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Hallucinated APIs: The “Ghost Methods” Problem
&lt;/h2&gt;

&lt;p&gt;AI sometimes invents functions that sound correct:&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;array_first_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ❌ does not exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you don’t recognize it, verify it.&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;“Which PHP version introduced this?”&lt;/li&gt;
&lt;li&gt;“Show official docs”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No answer → hallucination.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost: AI-Grown Code
&lt;/h2&gt;

&lt;p&gt;If AI writes most of your refactoring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code becomes harder to reason about&lt;/li&gt;
&lt;li&gt;intent disappears&lt;/li&gt;
&lt;li&gt;onboarding slows down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Countermeasure:&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;“Explain why this design was chosen over a simpler alternative.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the answer is vague → the design is weak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing AI Code Like a Senior
&lt;/h2&gt;

&lt;p&gt;Static analysis won’t save you.&lt;/p&gt;

&lt;p&gt;Tools like PHPStan check correctness—not intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually review
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Semantic correctness&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Did we simplify—or just move complexity?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Concurrency safety&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hidden state mutation&lt;/li&gt;
&lt;li&gt;ordering assumptions&lt;/li&gt;
&lt;li&gt;error propagation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask AI explicitly:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Simulate a timeout during async execution. What breaks?”&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026 Refactoring Checklist
&lt;/h2&gt;

&lt;p&gt;Before merging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type safety → strict, explicit&lt;/li&gt;
&lt;li&gt;No hidden shared state&lt;/li&gt;
&lt;li&gt;Async is intentional (not accidental)&lt;/li&gt;
&lt;li&gt;Dependencies are minimal and meaningful&lt;/li&gt;
&lt;li&gt;Errors are explicit (no silent nulls)&lt;/li&gt;
&lt;li&gt;You can explain every change&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;→ don’t merge it&lt;/p&gt;

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

&lt;p&gt;AI is an accelerator—but also a multiplier of mistakes.&lt;/p&gt;

&lt;p&gt;It tends to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add complexity&lt;/li&gt;
&lt;li&gt;over-engineer&lt;/li&gt;
&lt;li&gt;hide intent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your role is no longer just writing code.&lt;/p&gt;

&lt;p&gt;It’s &lt;strong&gt;curating it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The real senior skill in 2026 is knowing when to say:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“This is too clever. Make it simple.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because great refactoring isn’t about adding concurrency.&lt;/p&gt;

&lt;p&gt;It’s about &lt;strong&gt;removing everything that doesn’t need to be there&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>php</category>
      <category>refactorit</category>
    </item>
    <item>
      <title>Small Pull Requests: Why They Move Development Teams Faster</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 17 Mar 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/small-pull-requests-why-they-move-development-teams-faster-20kh</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/small-pull-requests-why-they-move-development-teams-faster-20kh</guid>
      <description>&lt;p&gt;In many backend teams, pull requests slowly grow into something nobody wants to review.&lt;/p&gt;

&lt;p&gt;A developer starts working on a feature.&lt;br&gt;
At first, it’s just a small change — maybe a new endpoint or a service update.&lt;/p&gt;

&lt;p&gt;Soon another improvement gets added.&lt;/p&gt;

&lt;p&gt;A small refactor follows.&lt;/p&gt;

&lt;p&gt;Finally, a fix for something unrelated appears.&lt;/p&gt;

&lt;p&gt;Two days later, the pull request contains &lt;strong&gt;900 lines of changes across 14 files.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At that moment, something predictable happens.&lt;/p&gt;

&lt;p&gt;The review slows down.&lt;/p&gt;

&lt;p&gt;Not because the reviewers are lazy — but because &lt;strong&gt;large pull requests create cognitive overload.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And once reviews slow down, the entire development workflow begins to lose momentum.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/02/21/why-just-one-more-quick-fix/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Psychological Problem of Large Pull Requests
&lt;/h2&gt;

&lt;p&gt;Large pull requests create a subtle psychological effect.&lt;/p&gt;

&lt;p&gt;When a reviewer opens a PR and sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+824 −217 changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;their brain immediately categorizes it as &lt;strong&gt;expensive work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The result is predictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The review gets postponed.&lt;/li&gt;
&lt;li&gt;The reviewer waits for “more time”.&lt;/li&gt;
&lt;li&gt;The PR sits for hours or days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, when someone finally reviews it, they cannot realistically analyze everything in depth.&lt;/p&gt;

&lt;p&gt;So one of two things happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The reviewer skims the code and approves it quickly.&lt;/li&gt;
&lt;li&gt;The reviewer leaves many comments that trigger long discussion threads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither outcome improves delivery speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Pull Requests Change the Dynamic
&lt;/h2&gt;

&lt;p&gt;Now compare that to a PR that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+42 −8 changes

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

&lt;/div&gt;



&lt;p&gt;This feels manageable.&lt;/p&gt;

&lt;p&gt;A reviewer can realistically read the entire diff in a few minutes.&lt;/p&gt;

&lt;p&gt;This leads to several important effects:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Faster Reviews
&lt;/h2&gt;

&lt;p&gt;Small PRs get reviewed faster simply because they feel easier.&lt;/p&gt;

&lt;p&gt;Many teams see review times drop from &lt;strong&gt;days to hours&lt;/strong&gt; after adopting smaller pull requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Better Feedback
&lt;/h2&gt;

&lt;p&gt;When the code is smaller, reviewers can focus on &lt;strong&gt;design decisions instead of scanning files.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of writing comments like:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“This file changed a lot, can you explain the logic?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;they can ask more meaningful questions:&lt;/p&gt;

&lt;p&gt;“&lt;em&gt;Should this logic live in the service layer instead?”&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Less Risk
&lt;/h2&gt;

&lt;p&gt;Large pull requests often hide bugs.&lt;/p&gt;

&lt;p&gt;For example, reviewers may miss subtle mistakes when hundreds of lines change at once.&lt;/p&gt;

&lt;p&gt;Small PRs isolate changes.&lt;/p&gt;

&lt;p&gt;If a bug appears, it’s easier to trace it back to a specific change.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Example
&lt;/h2&gt;

&lt;p&gt;Consider a backend developer implementing a new feature:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send notification emails when an order is shipped.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A typical large PR might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database migration&lt;/li&gt;
&lt;li&gt;new notification service&lt;/li&gt;
&lt;li&gt;email template&lt;/li&gt;
&lt;li&gt;queue job&lt;/li&gt;
&lt;li&gt;controller changes&lt;/li&gt;
&lt;li&gt;unit tests&lt;/li&gt;
&lt;li&gt;refactoring an old helper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All combined into &lt;strong&gt;one big pull request.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead, this can be split into several smaller ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR 1 – Database Migration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add shipped_at column to orders table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple schema change.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR 2 – Notification Service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create OrderShipmentNotifier service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure backend logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR 3 – Queue Job
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add SendShipmentEmail job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Background processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR 4 – Controller Integration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trigger notification when order is shipped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integration step.&lt;/p&gt;

&lt;p&gt;Each PR becomes &lt;strong&gt;reviewable within minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The entire feature moves through the pipeline &lt;strong&gt;much faster&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Benefit: Continuous Integration
&lt;/h2&gt;

&lt;p&gt;Small pull requests also improve CI pipelines.&lt;/p&gt;

&lt;p&gt;Large PRs often cause several problems:&lt;/p&gt;

&lt;p&gt;When a small PR fails CI, the cause is usually obvious.&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 plaintext"&gt;&lt;code&gt;PR title: Add OrderShipmentNotifier service
CI failure: NotificationServiceTest fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Compare this to a massive PR with dozens of changes — the failure may come from anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Resist Small Pull Requests
&lt;/h2&gt;

&lt;p&gt;Despite the benefits, many developers hesitate to split their work.&lt;/p&gt;

&lt;p&gt;Common reasons include:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;“The feature isn’t finished yet.”&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;This is the most common argument.&lt;/p&gt;

&lt;p&gt;But many PRs &lt;strong&gt;do not need to represent a finished feature.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They only need to represent a safe incremental step.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;You can merge a service class before it’s used anywhere.&lt;/p&gt;

&lt;p&gt;That’s perfectly valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;“It creates too many PRs.”&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;This concern sounds logical but rarely holds in practice.&lt;/p&gt;

&lt;p&gt;Ten small PRs usually move through the system faster than one large one.&lt;/p&gt;

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

&lt;p&gt;Because multiple reviewers can process them quickly instead of blocking on one huge change.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;“It’s extra work.”&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;At first, splitting work into smaller PRs requires discipline.&lt;/p&gt;

&lt;p&gt;But after a few weeks, it becomes natural.&lt;/p&gt;

&lt;p&gt;Developers start thinking in &lt;strong&gt;incremental changes&lt;/strong&gt; rather than large feature drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Rule That Works
&lt;/h2&gt;

&lt;p&gt;Some teams use a simple heuristic:&lt;/p&gt;

&lt;p&gt;If a pull request takes more than &lt;strong&gt;10 minutes to review&lt;/strong&gt;, it is probably too large.&lt;/p&gt;

&lt;p&gt;This isn’t a strict rule, but it works surprisingly well.&lt;/p&gt;

&lt;p&gt;Another guideline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200–300 lines of diff&lt;/strong&gt; should usually be the upper limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond that, review quality drops quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Long-Term Impact on Workflow
&lt;/h2&gt;

&lt;p&gt;When teams consistently use small pull requests, several improvements appear over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline becomes predictable
&lt;/h2&gt;

&lt;p&gt;PRs move steadily through the system instead of forming large queues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developers ship code more often
&lt;/h2&gt;

&lt;p&gt;Frequent merges reduce integration conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code reviews become collaborative
&lt;/h2&gt;

&lt;p&gt;Instead of being a bottleneck, reviews become quick feedback loops.&lt;/p&gt;

&lt;p&gt;And perhaps most importantly:&lt;/p&gt;

&lt;p&gt;The team starts focusing on &lt;strong&gt;continuous delivery instead of large feature dumps.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Many teams try to improve their development workflow by introducing new tools, processes, or complex CI pipelines.&lt;/p&gt;

&lt;p&gt;But one of the most effective improvements is surprisingly simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep pull requests small.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Small pull requests reduce friction in every stage of development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code review&lt;/li&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;li&gt;debugging&lt;/li&gt;
&lt;li&gt;merging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They allow teams to maintain momentum — and momentum is often the most valuable resource in software development.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>software</category>
      <category>ci</category>
      <category>programming</category>
    </item>
    <item>
      <title>Laravel Queue Testing: What Most Developers Get Wrong</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 10 Mar 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/laravel-queue-testing-what-most-developers-get-wrong-45oj</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/laravel-queue-testing-what-most-developers-get-wrong-45oj</guid>
      <description>&lt;p&gt;Queues are where “it works on my machine” quietly turns into production incidents.&lt;/p&gt;

&lt;p&gt;Emails are sent in the background.&lt;br&gt;
Invoices are generated asynchronously.&lt;br&gt;
External APIs are called outside the request lifecycle.&lt;br&gt;
Data synchronization runs in workers you don’t actively watch.&lt;/p&gt;

&lt;p&gt;Laravel makes queues extremely easy to use. Add ShouldQueue, dispatch the job, done.&lt;/p&gt;

&lt;p&gt;Testing them properly is a different story.&lt;/p&gt;

&lt;p&gt;Most developers stop at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bus::fake();
Bus::assertDispatched(SyncOrderJob::class);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That verifies wiring. It does &lt;strong&gt;not&lt;/strong&gt; verify behavior.&lt;/p&gt;

&lt;p&gt;This article focuses on how to test Laravel jobs correctly — including idempotency, failure handling, and retry safety — with a realistic example.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/02/14/contract-testing-external-apis-in-php-with-pact-real-laravel-example/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scenario: Syncing an Order to an External API
&lt;/h2&gt;

&lt;p&gt;Let’s say we have an Order model that needs to be synchronized to an external system after checkout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Job&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace App\Jobs;use App\Models\Order;
use App\Services\OrderApiClient;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;class SyncOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, SerializesModels;    public int $tries = 3;    public function __construct(
        public Order $order
    ) {}    public function handle(OrderApiClient $client): void
    {
        if ($this-&amp;gt;order-&amp;gt;synced_at !== null) {
            return; // idempotency guard
        }        $client-&amp;gt;send([
            'id' =&amp;gt; $this-&amp;gt;order-&amp;gt;id,
            'total' =&amp;gt; $this-&amp;gt;order-&amp;gt;total,
        ]);        $this-&amp;gt;order-&amp;gt;update([
            'synced_at' =&amp;gt; now(),
        ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Already we have several important concerns:&lt;/p&gt;

&lt;p&gt;The job must not sync twice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It depends on an external API client.&lt;/li&gt;
&lt;li&gt;It may be retried.&lt;/li&gt;
&lt;li&gt;It mutates persistent state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s test it properly&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Testing Dispatch (Integration Level)
&lt;/h2&gt;

&lt;p&gt;This belongs in a feature test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use Illuminate\Support\Facades\Bus;
use App\Jobs\SyncOrderJob;

public function test_order_dispatches_sync_job()
{
    Bus::fake();

    $order = Order::factory()-&amp;gt;create();

    $order-&amp;gt;markAsPaid(); // imagine this dispatches the job

    Bus::assertDispatched(SyncOrderJob::class, function ($job) use ($order) {
        return $job-&amp;gt;order-&amp;gt;is($order);
    });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good. Necessary.&lt;/p&gt;

&lt;p&gt;But insufficient.&lt;/p&gt;

&lt;p&gt;This does not test &lt;em&gt;handle()&lt;/em&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Testing Job Logic Directly (Unit Level)
&lt;/h2&gt;

&lt;p&gt;Now we test the job itself.&lt;/p&gt;

&lt;p&gt;We do not fake the bus.&lt;br&gt;
We instantiate the job and call handle() directly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Mocking the External API
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use App\Services\OrderApiClient;
use Mockery;

public function test_it_calls_external_api_when_not_synced()
{
    $order = Order::factory()-&amp;gt;create([
        'synced_at' =&amp;gt; null,
    ]);    

    $mock = Mockery::mock(OrderApiClient::class);

    $mock-&amp;gt;shouldReceive('send')
        -&amp;gt;once()
        -&amp;gt;with(Mockery::on(fn ($payload) =&amp;gt; $payload['id'] === $order-&amp;gt;id));    

    $job = new SyncOrderJob($order);    

    $job-&amp;gt;handle($mock);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This verifies real behavior.&lt;/p&gt;

&lt;p&gt;We are not testing Laravel.&lt;br&gt;
We are testing our business logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Testing Idempotency
&lt;/h2&gt;

&lt;p&gt;Queues retry automatically. If your job is not idempotent, you will create duplicate side effects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function test_it_does_not_call_api_if_already_synced()
{
    $order = Order::factory()-&amp;gt;create([
        'synced_at' =&amp;gt; now(),
    ]);    

    $mock = Mockery::mock(OrderApiClient::class);

    $mock-&amp;gt;shouldNotReceive('send');    

    $job = new SyncOrderJob($order);    

    $job-&amp;gt;handle($mock);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this test fails, your production system will eventually duplicate work.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Testing Retry Safety
&lt;/h2&gt;

&lt;p&gt;Consider a failure between the API call and the database update.&lt;/p&gt;

&lt;p&gt;If the job crashes after calling the API but before marking the order as synced, retry will send it again.&lt;/p&gt;

&lt;p&gt;Safer pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function handle(OrderApiClient $client): void
{
    if ($this-&amp;gt;order-&amp;gt;synced_at !== null) {
        return;
    }    $this-&amp;gt;order-&amp;gt;update([
        'synced_at' =&amp;gt; now(),
    ]);    $client-&amp;gt;send([
        'id' =&amp;gt; $this-&amp;gt;order-&amp;gt;id,
        'total' =&amp;gt; $this-&amp;gt;order-&amp;gt;total,
    ]);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now state changes before side effects.&lt;/p&gt;

&lt;p&gt;Let’s simulate a failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function test_it_retries_safely()
{
    $order = Order::factory()-&amp;gt;create([
        'synced_at' =&amp;gt; null,
    ]);

    $mock = Mockery::mock(OrderApiClient::class);
    $mock-&amp;gt;shouldReceive('send')
        -&amp;gt;once()
        -&amp;gt;andThrow(new RuntimeException());

    $job = new SyncOrderJob($order);

    try {
        $job-&amp;gt;handle($mock);
    } catch (RuntimeException $e) {
        // expected
    }

    $order-&amp;gt;refresh();

    $this-&amp;gt;assertNotNull($order-&amp;gt;synced_at);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now even if retry happens, the idempotency guard prevents double execution.&lt;/p&gt;

&lt;p&gt;This is not theoretical. This is how duplicate invoices happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Testing Jobs That Dispatch Other Jobs
&lt;/h2&gt;

&lt;p&gt;Chained or nested jobs add complexity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ProcessPaymentJob::dispatch($order);
SendInvoiceJob::dispatch($order);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test that correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bus::fake();

$job = new ProcessPaymentJob($order);

$job-&amp;gt;handle($paymentService);

Bus::assertDispatched(SendInvoiceJob::class);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We test &lt;em&gt;handle()&lt;/em&gt; directly.&lt;/li&gt;
&lt;li&gt;We fake the bus only to assert nested dispatch.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  6. Testing Backoff and Retry Configuration
&lt;/h2&gt;

&lt;p&gt;Laravel allows configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public int $tries = 5;

public function backoff(): array
{
    return [10, 30, 60];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can test configuration explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function test_job_has_correct_retry_settings()
{
    $job = new SyncOrderJob(Order::factory()-&amp;gt;make());

    $this-&amp;gt;assertEquals(3, $job-&amp;gt;tries);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not glamorous — but configuration errors cause real outages.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The Dangerous Anti-Pattern
&lt;/h2&gt;

&lt;p&gt;This is common:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queue::fake();

SyncOrderJob::dispatch($order);

Queue::assertPushed(SyncOrderJob::class);
&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 job throws immediately.&lt;/li&gt;
&lt;li&gt;The API client is broken.&lt;/li&gt;
&lt;li&gt;The logic is inverted.&lt;/li&gt;
&lt;li&gt;Idempotency is missing.&lt;/li&gt;
&lt;li&gt;The job deletes the order by accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You tested dispatch, not behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use &lt;em&gt;Bus::fake()&lt;/em&gt; vs Real Execution
&lt;/h2&gt;

&lt;p&gt;Use &lt;em&gt;Bus::fake()&lt;/em&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing controllers or services that dispatch jobs.&lt;/li&gt;
&lt;li&gt;Verifying orchestration.&lt;/li&gt;
&lt;li&gt;Ensuring a job is queued under certain conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not use &lt;em&gt;Bus::fake()&lt;/em&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing job business logic.&lt;/li&gt;
&lt;li&gt;Testing external integration behavior.&lt;/li&gt;
&lt;li&gt;Testing failure handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In job tests, instantiate and call &lt;em&gt;handle()&lt;/em&gt; directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: Running Jobs Synchronously in Tests
&lt;/h2&gt;

&lt;p&gt;Laravel allows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config(['queue.default' =&amp;gt; 'sync']);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This executes jobs immediately.&lt;/p&gt;

&lt;p&gt;Useful for feature tests where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want full behavior executed.&lt;/li&gt;
&lt;li&gt;You don’t care about queue infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But be careful:&lt;/p&gt;

&lt;p&gt;If you rely on sync execution everywhere, you may miss race conditions or retry issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Principles for Testable Jobs
&lt;/h2&gt;

&lt;p&gt;If your job is hard to test, it’s usually poorly designed.&lt;/p&gt;

&lt;p&gt;Good Laravel jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inject services via handle().&lt;/li&gt;
&lt;li&gt;Keep constructors lightweight.&lt;/li&gt;
&lt;li&gt;Avoid heavy logic in __construct.&lt;/li&gt;
&lt;li&gt;Avoid static service calls.&lt;/li&gt;
&lt;li&gt;Are idempotent by design.&lt;/li&gt;
&lt;li&gt;Fail loudly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad Laravel jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Call Http::post() directly inside handle.&lt;/li&gt;
&lt;li&gt;Query models statically without abstraction.&lt;/li&gt;
&lt;li&gt;Mutate global state.&lt;/li&gt;
&lt;li&gt;Swallow exceptions silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing reveals architecture quality.&lt;/p&gt;

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

&lt;p&gt;Queues introduce three new axes of complexity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Time (execution is delayed)&lt;/li&gt;
&lt;li&gt;Failure (automatic retries)&lt;/li&gt;
&lt;li&gt;Concurrency (multiple workers)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Testing Laravel jobs properly means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing dispatch at feature level.&lt;/li&gt;
&lt;li&gt;Testing handle() directly at unit level.&lt;/li&gt;
&lt;li&gt;Mocking external services.&lt;/li&gt;
&lt;li&gt;Verifying idempotency.&lt;/li&gt;
&lt;li&gt;Simulating failure.&lt;/li&gt;
&lt;li&gt;Thinking explicitly about retry safety.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Queues scale your application.&lt;/p&gt;

&lt;p&gt;Tests make your background processing reliable.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>laravel</category>
      <category>php</category>
      <category>testing</category>
    </item>
    <item>
      <title>Singleton Pattern in PHP: Refactoring Global State the Right Way</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 03 Mar 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/singleton-pattern-in-php-refactoring-global-state-the-right-way-1gbl</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/singleton-pattern-in-php-refactoring-global-state-the-right-way-1gbl</guid>
      <description>&lt;p&gt;In many legacy PHP codebases, global state sneaks in quietly.&lt;br&gt;
A config.php file is included everywhere.&lt;br&gt;
A static helper class grows until it becomes a god object.&lt;br&gt;
A database connection lives in a global variable “just for now”.&lt;/p&gt;

&lt;p&gt;At first, it feels convenient.&lt;br&gt;
Later, it becomes untestable, unpredictable, and painful to change.&lt;/p&gt;

&lt;p&gt;Let’s look at how global state often appears in PHP projects, how developers usually try to “fix” it, and how refactoring toward a Singleton can help — but also why Singleton is frequently misused.&lt;/p&gt;

&lt;p&gt;Previously article in this category: &lt;a href="https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Code Smell: Hidden Global State
&lt;/h2&gt;

&lt;p&gt;A typical example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// config.php
return [
    'db_host' =&amp;gt; 'localhost',
    'db_user' =&amp;gt; 'root',
    'db_pass' =&amp;gt; 'secret',
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// bootstrap.php
$config = require 'config.php';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// UserRepository.php
class UserRepository
{
    public function find(int $id): array
    {
        global $config;        $conn = new PDO(
            'mysql:host=' . $config['db_host'],
            $config['db_user'],
            $config['db_pass']
        );        return $conn-&amp;gt;query('SELECT * FROM users WHERE id = ' . $id)-&amp;gt;fetch();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code “works”.&lt;br&gt;
It also hides a dependency.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;UserRepository&lt;/em&gt; depends on configuration and a database connection, but nothing in its API communicates that. The dependency is invisible and implicit.&lt;/p&gt;

&lt;p&gt;This leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unpredictable behavior in tests&lt;/li&gt;
&lt;li&gt;coupling between unrelated parts of the system&lt;/li&gt;
&lt;li&gt;accidental mutation of shared state&lt;/li&gt;
&lt;li&gt;code that is hard to reason about&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a classic code smell: &lt;strong&gt;hidden dependencies via global state.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Naive Fix: Static Helper Class
&lt;/h2&gt;

&lt;p&gt;The next evolutionary step in many PHP projects looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Config
{
    private static array $data;    public static function load(): void
    {
        self::$data = require 'config.php';
    }    public static function get(string $key): mixed
    {
        return self::$data[$key];
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class UserRepository
{
    public function find(int $id): array
    {
        $conn = new PDO(
            'mysql:host=' . Config::get('db_host'),
            Config::get('db_user'),
            Config::get('db_pass')
        );        return $conn-&amp;gt;query('SELECT * FROM users WHERE id = ' . $id)-&amp;gt;fetch();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks cleaner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no global&lt;/li&gt;
&lt;li&gt;no messy includes&lt;/li&gt;
&lt;li&gt;centralized config access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But architecturally, nothing really improved.&lt;/p&gt;

&lt;p&gt;This is still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;global state&lt;/li&gt;
&lt;li&gt;hidden dependency&lt;/li&gt;
&lt;li&gt;impossible to replace in tests&lt;/li&gt;
&lt;li&gt;tightly coupled to a static API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We replaced global with a static singleton-like façade.&lt;/p&gt;

&lt;p&gt;This is not refactoring.&lt;br&gt;
This is just changing syntax.&lt;/p&gt;
&lt;h2&gt;
  
  
  Refactoring Toward a Real Singleton
&lt;/h2&gt;

&lt;p&gt;Let’s refactor this step by step into a proper Singleton.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class Config
{
    private static ?Config $instance = null;
    private array $data;    private function __construct()
    {
        $this-&amp;gt;data = require 'config.php';
    }    public static function getInstance(): Config
    {
        if (self::$instance === null) {
            self::$instance = new Config();
        }        return self::$instance;
    }    public function get(string $key): mixed
    {
        return $this-&amp;gt;data[$key] ?? null;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class UserRepository
{
    public function find(int $id): array
    {
        $config = Config::getInstance();        $conn = new PDO(
            'mysql:host=' . $config-&amp;gt;get('db_host'),
            $config-&amp;gt;get('db_user'),
            $config-&amp;gt;get('db_pass')
        );        return $conn-&amp;gt;query('SELECT * FROM users WHERE id = ' . $id)-&amp;gt;fetch();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is now a “proper” Singleton:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;private constructor&lt;/li&gt;
&lt;li&gt;lazy initialization&lt;/li&gt;
&lt;li&gt;controlled access point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We improved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;structure&lt;/li&gt;
&lt;li&gt;encapsulation&lt;/li&gt;
&lt;li&gt;testability (slightly)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But we did not fix the core design issue.&lt;/p&gt;

&lt;p&gt;The dependency is still hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem: Singleton Is Global State in Disguise
&lt;/h2&gt;

&lt;p&gt;A Singleton is not dependency injection.&lt;br&gt;
It is &lt;strong&gt;global state with better manners&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You still cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pass different configurations in tests&lt;/li&gt;
&lt;li&gt;swap implementations easily&lt;/li&gt;
&lt;li&gt;reason locally about dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From an architectural perspective, &lt;em&gt;UserRepository&lt;/em&gt; still depends on &lt;em&gt;Config&lt;/em&gt;, but that dependency is invisible in its constructor or method signature.&lt;/p&gt;

&lt;p&gt;This creates tight coupling and invisible control flow.&lt;/p&gt;

&lt;p&gt;The code is cleaner — but not better designed.&lt;/p&gt;
&lt;h2&gt;
  
  
  When Singleton Is Actually Legitimate
&lt;/h2&gt;

&lt;p&gt;Singleton is not evil by definition.&lt;br&gt;
It is evil when used as a default container for everything.&lt;/p&gt;

&lt;p&gt;Legitimate use cases in PHP:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Immutable Application Configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class AppConfig
{
    private static ?AppConfig $instance = null;
    private array $values;    private function __construct(array $values)
    {
        $this-&amp;gt;values = $values;
    }    public static function boot(array $values): void
    {
        self::$instance = new self($values);
    }    public static function getInstance(): AppConfig
    {
        return self::$instance;
    }    public function get(string $key): mixed
    {
        return $this-&amp;gt;values[$key] ?? null;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bootstrapped once, read-only afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Logger&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A logger is often a cross-cutting concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shared&lt;/li&gt;
&lt;li&gt;stateless&lt;/li&gt;
&lt;li&gt;global by nature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Feature Flags / Environment Context&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the state is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;immutable&lt;/li&gt;
&lt;li&gt;read-only&lt;/li&gt;
&lt;li&gt;environment-level&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Singleton can be acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Singleton Is a Design Smell
&lt;/h2&gt;

&lt;p&gt;Singleton becomes a problem when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it contains mutable domain state&lt;/li&gt;
&lt;li&gt;it replaces proper dependency injection&lt;/li&gt;
&lt;li&gt;it hides real architectural boundaries&lt;/li&gt;
&lt;li&gt;it becomes a service locator&lt;/li&gt;
&lt;li&gt;business logic starts living inside it&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;20 methods&lt;/li&gt;
&lt;li&gt;multiple responsibilities&lt;/li&gt;
&lt;li&gt;domain logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you created a god object with a private constructor.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Refactoring Path
&lt;/h2&gt;

&lt;p&gt;Often, Singleton should be treated as a &lt;strong&gt;transitional refactoring step&lt;/strong&gt;, not a final architecture.&lt;/p&gt;

&lt;p&gt;Better end state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class UserRepository
{
    public function __construct(
        private PDO $connection
    ) {}    public function find(int $id): array
    {
        return $this-&amp;gt;connection
            -&amp;gt;query('SELECT * FROM users WHERE id = ' . $id)
            -&amp;gt;fetch();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And composition happens at the boundary of the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$config = Config::getInstance();$pdo = new PDO(
    'mysql:host=' . $config-&amp;gt;get('db_host'),
    $config-&amp;gt;get('db_user'),
    $config-&amp;gt;get('db_pass')
);$repo = new UserRepository($pdo);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dependencies are explicit&lt;/li&gt;
&lt;li&gt;testing is trivial&lt;/li&gt;
&lt;li&gt;coupling is controlled&lt;/li&gt;
&lt;li&gt;architecture is visible&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Singleton is not a goal.&lt;br&gt;
It is a compromise.&lt;/p&gt;

&lt;p&gt;It can be a useful refactoring step when removing global state from a legacy PHP codebase.&lt;br&gt;
It can make dependencies slightly more explicit.&lt;br&gt;
It can improve encapsulation.&lt;/p&gt;

&lt;p&gt;But if you stop there, you only replaced chaos with a nicer-looking global variable.&lt;/p&gt;

&lt;p&gt;Good design does not start with patterns.&lt;br&gt;
It starts with &lt;strong&gt;making dependencies explicit&lt;/strong&gt; and &lt;strong&gt;pushing composition to the edges of your system&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Singleton is a tool.&lt;br&gt;
Use it deliberately — and sparingly.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>softwareengineering</category>
      <category>php</category>
      <category>refactoring</category>
    </item>
    <item>
      <title>Why “Just One More Quick Fix” Destroys Your Delivery Pipeline</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 24 Feb 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/why-just-one-more-quick-fix-destroys-your-delivery-pipeline-4927</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/why-just-one-more-quick-fix-destroys-your-delivery-pipeline-4927</guid>
      <description>&lt;p&gt;Every development team has heard this sentence:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Let’s just push one more quick fix.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It sounds harmless. Responsible, even. Something small broke, a customer is affected, production is slightly off — and the fix is obvious. Five minutes of work. No need to go through the full process. No need to wait for code review. No need to bother QA. Just one tiny change to keep things moving.&lt;/p&gt;

&lt;p&gt;The problem is not the quick fix itself.&lt;br&gt;
The problem is the culture that grows around it.&lt;/p&gt;

&lt;p&gt;Over time, “just one more quick fix” becomes the default way changes enter production. And once that happens, your delivery pipeline slowly collapses — not because anyone is incompetent, but because the system you built to protect quality gets bypassed piece by piece.&lt;/p&gt;

&lt;p&gt;Previous article in this category: &lt;a href="https://codecraftdiary.com/2026/01/31/why-almost-done-work-breaks-development-flow/" rel="noopener noreferrer"&gt;https://codecraftdiary.com/2026/01/31/why-almost-done-work-breaks-development-flow/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Quick Fixes Become the Normal Path to Production
&lt;/h2&gt;

&lt;p&gt;Most teams don’t start with bad intentions. The first few quick fixes usually happen under real pressure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A critical bug blocks a customer.&lt;/li&gt;
&lt;li&gt;A payment fails.&lt;/li&gt;
&lt;li&gt;An integration with a third-party API breaks overnight.&lt;/li&gt;
&lt;li&gt;A configuration mistake takes down part of production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this stage, bypassing the process feels justified. You skip review because “it’s just a one-line change.” You deploy manually because the CI pipeline takes too long. You test in production because the bug only reproduces there.&lt;/p&gt;

&lt;p&gt;Nothing explodes. The fix works. The customer is happy. The team moves on.&lt;/p&gt;

&lt;p&gt;That success teaches the wrong lesson.&lt;/p&gt;

&lt;p&gt;The next time something small breaks, the team remembers:&lt;br&gt;
“We fixed it quickly last time. Let’s do it the same way.”&lt;/p&gt;

&lt;p&gt;After a few months, you end up with two delivery paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The official pipeline: PR → review → tests → CI → deploy&lt;/li&gt;
&lt;li&gt;The real pipeline: Slack message → quick commit → direct deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second one becomes the faster, socially accepted option. The first one starts to feel like bureaucracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Damage: You Don’t See the Cost Immediately
&lt;/h2&gt;

&lt;p&gt;The real damage of quick fixes is not in the individual changes. It’s in what they do to your system over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Your CI Pipeline Stops Being Trustworthy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once people regularly bypass CI, the pipeline stops reflecting reality. Builds fail for reasons nobody prioritizes fixing. Tests become flaky and get ignored. Warnings pile up.&lt;/p&gt;

&lt;p&gt;Eventually, developers stop trusting the pipeline at all.&lt;/p&gt;

&lt;p&gt;At that point, CI is no longer a quality gate — it’s just a ritual.&lt;br&gt;
Teams running on platforms like GitHub or GitLab often reach this state not because the tools are bad, but because the workflow culture quietly degrades around them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Code Reviews Become Optional (Which Means Useless)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When urgent fixes bypass review, reviews lose their authority. Review becomes something you do for “normal work,” while “important work” skips it.&lt;/p&gt;

&lt;p&gt;That’s a subtle but dangerous shift.&lt;/p&gt;

&lt;p&gt;You’re effectively teaching the team that quality checks are less important when stakes are higher. Over time, this creates blind spots exactly where you need discipline the most: production-critical paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You Normalize Risk Without Noticing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every quick fix that works without immediate consequences normalizes risk-taking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploying without review becomes acceptable.&lt;/li&gt;
&lt;li&gt;Deploying without tests becomes acceptable.&lt;/li&gt;
&lt;li&gt;Deploying without understanding side effects becomes 
acceptable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The absence of incidents is interpreted as proof that the process is unnecessary — until the day it fails in a way that costs real money, data, or trust.&lt;/p&gt;

&lt;p&gt;By the time that incident happens, the delivery pipeline is already too eroded to protect you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Psychological Trap: Quick Fixes Feel Like Ownership
&lt;/h2&gt;

&lt;p&gt;Quick fixes often feel good on a personal level.&lt;/p&gt;

&lt;p&gt;You fixed a production issue fast.&lt;br&gt;
You unblocked a customer.&lt;br&gt;
You were the hero who saved the day.&lt;/p&gt;

&lt;p&gt;This creates a subtle incentive problem: the system rewards individuals for bypassing the process. The delivery pipeline, which exists to protect the team and the product, becomes an obstacle to individual heroics.&lt;/p&gt;

&lt;p&gt;Over time, teams drift into a culture where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Being fast matters more than being safe.&lt;/li&gt;
&lt;li&gt;Fixing symptoms is rewarded more than fixing root causes.&lt;/li&gt;
&lt;li&gt;Short-term relief beats long-term stability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how delivery pipelines rot — not from neglect, but from well-meaning people optimizing for speed under pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Pattern: The “Temporary” Hotfix That Becomes Permanent
&lt;/h2&gt;

&lt;p&gt;A common pattern looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A bug appears in production.&lt;/li&gt;
&lt;li&gt;Someone applies a quick fix directly in production or through a rushed commit.&lt;/li&gt;
&lt;li&gt;The fix is labeled “temporary.”&lt;/li&gt;
&lt;li&gt;The team plans to clean it up later.&lt;/li&gt;
&lt;li&gt;Later never comes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weeks later, another change interacts with that temporary fix and breaks something else. Now nobody fully understands the code path. The pipeline didn’t catch it because the fix never went through proper tests.&lt;/p&gt;

&lt;p&gt;The original quick fix saved 30 minutes.&lt;br&gt;
The long-term cost is measured in hours of debugging, fragile code, and lost confidence in the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Destroys Flow (Not Just Quality)
&lt;/h2&gt;

&lt;p&gt;Quick fixes don’t just hurt quality. They destroy delivery flow.&lt;/p&gt;

&lt;p&gt;Once bypassing the pipeline becomes normal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers hesitate to touch fragile areas.&lt;/li&gt;
&lt;li&gt;Releases become unpredictable.&lt;/li&gt;
&lt;li&gt;Small changes cause big side effects.&lt;/li&gt;
&lt;li&gt;People slow down because they don’t trust the system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ironically, the behavior that started as “moving faster” ends up making delivery slower and more stressful.&lt;/p&gt;

&lt;p&gt;You trade short-term speed for long-term friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Fix the Culture Without Becoming Bureaucratic
&lt;/h2&gt;

&lt;p&gt;This isn’t about banning hotfixes. Production incidents are real. Speed matters. The goal is to make &lt;em&gt;fast&lt;/em&gt; changes also &lt;em&gt;safe&lt;/em&gt; changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Make the Fast Path the Safe Path&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your CI takes 40 minutes, people will bypass it.&lt;br&gt;
If your review process blocks urgent fixes, people will bypass it.&lt;/p&gt;

&lt;p&gt;Your pipeline must be optimized for speed in emergencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lightweight reviews for hotfixes.&lt;/li&gt;
&lt;li&gt;Fast CI path for critical changes.&lt;/li&gt;
&lt;li&gt;Clear “hotfix” workflow that still enforces minimal quality gates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Track Bypasses as Technical Debt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every bypass should leave a visible trace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A ticket to add tests later.&lt;/li&gt;
&lt;li&gt;A follow-up task to clean up the quick fix.&lt;/li&gt;
&lt;li&gt;A post-incident note explaining what was skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If bypassing the pipeline leaves no trace, it becomes invisible debt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Reward Stability, Not Heroics&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Publicly recognize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixes that go through the proper pipeline under pressure.&lt;/li&gt;
&lt;li&gt;Improvements to CI speed.&lt;/li&gt;
&lt;li&gt;Reductions in flaky tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop glorifying the person who “just pushed a fix to prod” without safeguards. You want to reward people who improve the system, not just fight its symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Insight
&lt;/h2&gt;

&lt;p&gt;“Just one more quick fix” is never just one.&lt;/p&gt;

&lt;p&gt;It’s a small exception that rewires how your team relates to the delivery pipeline.&lt;br&gt;
It teaches people what the real process is — not the one in documentation, but the one that gets things shipped.&lt;/p&gt;

&lt;p&gt;If your real process bypasses quality gates under pressure, then under real pressure, you don’t have a delivery pipeline at all.&lt;/p&gt;

&lt;p&gt;You have hope and heroics.&lt;/p&gt;

&lt;p&gt;And that’s not a strategy.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>development</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Contract Testing External APIs in PHP with Pact (Real Laravel Example)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Wed, 18 Feb 2026 14:05:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/contract-testing-external-apis-in-php-with-pact-real-laravel-example-o3g</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/contract-testing-external-apis-in-php-with-pact-real-laravel-example-o3g</guid>
      <description>&lt;p&gt;Testing integrations with external APIs is one of the most fragile parts of any web application. In theory, we write tests, mock HTTP clients, and feel confident. In practice, APIs change, fields disappear, status codes shift, and production breaks anyway.&lt;/p&gt;

&lt;p&gt;I learned this the hard way.&lt;/p&gt;

&lt;p&gt;In one project, all my tests were green. My mocks returned exactly what I expected. A week later, the external API removed one field from the response. My mocks didn’t know about the change. Production did.&lt;/p&gt;

&lt;p&gt;Mocks told me everything was fine. Reality disagreed.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of problem &lt;strong&gt;contract testing&lt;/strong&gt; is meant to solve. In this article, I’ll show you how to use contract testing for external APIs in PHP with &lt;strong&gt;Pact&lt;/strong&gt;, using a real-world *&lt;em&gt;Laravel *&lt;/em&gt; example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Mocking External APIs Is Not Enough
&lt;/h2&gt;

&lt;p&gt;Mocking external APIs is useful. I still do it. It makes tests fast, deterministic, and cheap. But mocks have one fatal flaw:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;They only test your assumptions about the API, not the API itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Typical problems I’ve seen in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API removes or renames a field.&lt;/li&gt;
&lt;li&gt;A nested structure changes shape.&lt;/li&gt;
&lt;li&gt;The API starts returning 404 instead of 200 in some edge cases.&lt;/li&gt;
&lt;li&gt;The API adds a required field to the request body.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your mocks will never catch this unless you manually update them. That means mocks alone can silently drift away from reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Contract Testing (In Plain English)
&lt;/h2&gt;

&lt;p&gt;Contract testing sits between mocking and full integration tests.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;“This is what I think the API returns,”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;you say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This is the contract between my app (consumer) and the API (provider).”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With consumer-driven contract testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;consumer&lt;/strong&gt;&lt;br&gt;
defines what it expects from the API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;provider&lt;/strong&gt; must verify that it actually fulfills this contract.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the provider changes something that breaks the contract, tests fail &lt;strong&gt;before production breaks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This turns breaking API changes from a runtime surprise into a build-time failure.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: How contract testing works between a Laravel consumer, Pact mock server, and the real API provider.&lt;/em&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Real-World Scenario: Laravel as API Consumer
&lt;/h2&gt;

&lt;p&gt;Let’s assume a Laravel application integrates with an external billing API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/customers/{id}

Response:
{
  "id": 123,
  "email": "john@example.com",
  "is_active": true
}

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

&lt;/div&gt;



&lt;p&gt;Your Laravel service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class BillingClient
{
    public function getCustomer(int $id): array
    {
        $response = Http::get("https://billing.example.com/api/customers/{$id}");

        return $response-&amp;gt;json();
    }
}

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

&lt;/div&gt;



&lt;p&gt;Your application relies on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email&lt;/li&gt;
&lt;li&gt;is_active&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the provider removes &lt;em&gt;is_active&lt;/em&gt;, your app breaks. Mocks won’t catch it. Contract testing will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step: Contract Testing with Pact in Laravel
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Installing Pact for PHP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install Pact dependencies for your test environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composer require --dev pact-foundation/pact-php

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

&lt;/div&gt;



&lt;p&gt;You’ll also need the Pact CLI running locally or in CI (usually via Docker).&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the Consumer Contract Test
&lt;/h2&gt;

&lt;p&gt;The goal: define what your Laravel app &lt;strong&gt;expects&lt;/strong&gt; from the API.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function test_it_fetches_customer_from_billing_api()
{
    $builder = new InteractionBuilder();

    $builder
        -&amp;gt;given('Customer 123 exists')
        -&amp;gt;uponReceiving('A request for customer 123')
        -&amp;gt;with([
            'method' =&amp;gt; 'GET',
            'path'   =&amp;gt; '/api/customers/123',
        ])
        -&amp;gt;willRespondWith([
            'status' =&amp;gt; 200,
            'headers' =&amp;gt; ['Content-Type' =&amp;gt; 'application/json'],
            'body' =&amp;gt; [
                'id' =&amp;gt; 123,
                'email' =&amp;gt; 'john@example.com',
                'is_active' =&amp;gt; true,
            ],
        ]);

    $builder-&amp;gt;verify(function () {
        $client = new BillingClient();
        $customer = $client-&amp;gt;getCustomer(123);

        $this-&amp;gt;assertSame('john@example.com', $customer['email']);
        $this-&amp;gt;assertTrue($customer['is_active']);
    });
}

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

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;defines the expected request&lt;/li&gt;
&lt;li&gt;defines the expected response&lt;/li&gt;
&lt;li&gt;generates a &lt;strong&gt;contract file&lt;/strong&gt; (Pact file)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This contract is the &lt;strong&gt;single source of truth&lt;/strong&gt; between consumer and provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the Contract on the Provider Side
&lt;/h2&gt;

&lt;p&gt;On the provider side, the API team runs Pact verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pact-verifier --provider-base-url=http://localhost:8000 \
 --pact-url=./pacts/billing-consumer.json

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

&lt;/div&gt;



&lt;p&gt;If the API no longer returns is_active, verification fails.&lt;/p&gt;

&lt;p&gt;This is the key difference from mocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mocks only test the consumer&lt;/li&gt;
&lt;li&gt;contract testing tests the agreement&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running Contract Tests in CI
&lt;/h2&gt;

&lt;p&gt;This is where contract testing becomes powerful.&lt;/p&gt;

&lt;p&gt;Typical CI flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consumer tests generate Pact contracts.&lt;/li&gt;
&lt;li&gt;Provider pipeline verifies contracts.&lt;/li&gt;
&lt;li&gt;Pipeline fails if contracts are broken.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This creates a safety net:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;API changes cannot be deployed if they break existing consumers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common Mistakes with Contract Testing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Over-Specifying Responses&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don’t lock down every field if you don’t need it.&lt;br&gt;
Only define what your app actually uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only Testing Happy Paths&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Include error contracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;404 responses&lt;/li&gt;
&lt;li&gt;validation errors&lt;/li&gt;
&lt;li&gt;unauthorized responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not Versioning Contracts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Treat contracts like code. Version them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not Enforcing Contracts in CI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If no pipeline enforces them, contract testing is just documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Contract Testing Is Worth It (and When It’s Overkill)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Contract testing is worth it when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple teams own different services&lt;/li&gt;
&lt;li&gt;APIs evolve frequently&lt;/li&gt;
&lt;li&gt;breaking changes are expensive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;It’s probably overkill when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the integration is trivial&lt;/li&gt;
&lt;li&gt;you don’t control the provider&lt;/li&gt;
&lt;li&gt;the API is stable and rarely changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contract testing is a scalpel, not a hammer. Use it where API stability matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Contract Testing Fits into Your Laravel Testing Strategy
&lt;/h2&gt;

&lt;p&gt;This is how I think about testing layers in real projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit tests – business logic&lt;/li&gt;
&lt;li&gt;Feature tests – HTTP flows&lt;/li&gt;
&lt;li&gt;Mocks – isolate slow or unstable services&lt;/li&gt;
&lt;li&gt;Contract tests – protect integrations from breaking changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mocks keep your tests fast.&lt;br&gt;
Contract tests keep your integrations honest.&lt;/p&gt;

&lt;p&gt;They solve different problems and work best together.&lt;/p&gt;

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

&lt;p&gt;Mocking external APIs is necessary — but it’s not sufficient.&lt;/p&gt;

&lt;p&gt;If your application depends on external services and you’ve ever been surprised by a breaking API change in production, contract testing is the missing safety net.&lt;/p&gt;

&lt;p&gt;You don’t need to contract-test everything. Start with one critical integration. Add one contract. Let your CI enforce it. The first time a breaking change is caught before deployment, contract testing pays for itself.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>laravel</category>
      <category>php</category>
    </item>
  </channel>
</rss>
