<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CodeCraft Diary</title>
    <description>The latest articles on DEV Community by CodeCraft Diary (@codecraft_diary_3d13677fb).</description>
    <link>https://dev.to/codecraft_diary_3d13677fb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3572568%2F9b328bfe-d229-4c84-8915-9af499c7bff0.png</url>
      <title>DEV Community: CodeCraft Diary</title>
      <link>https://dev.to/codecraft_diary_3d13677fb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codecraft_diary_3d13677fb"/>
    <language>en</language>
    <item>
      <title>Automated Testing Strategies for Trunk-Based Development: How to Build a 10-Minute Pipeline Home Development Workflow &amp; Best Practices</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 30 Jun 2026 14:44:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline-home-1520</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/automated-testing-strategies-for-trunk-based-development-how-to-build-a-10-minute-pipeline-home-1520</guid>
      <description>&lt;p&gt;If you’ve been following my recent posts, you already know I am a big fan of Trunk-Based Development (TBD), tiny pull requests, and deploying to production multiple times a day using feature flags. It sounds like an engineering dream: no long-lived branches, no merge hell on Friday afternoons, and features reaching users in hours instead of months.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Flaky tests are tests that sometimes pass and sometimes fail without any meaningful code changes. They are one of the most frustrating problems in modern software development because they slowly destroy trust in your test suite.&lt;/p&gt;

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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




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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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




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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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




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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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




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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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




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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;p&gt;I covered this topic in more detail in my API mocking article because external integrations are one of the easiest ways to accidentally create unstable tests.&lt;/p&gt;




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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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




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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;When PHP introduced native Enums, they became the perfect fit for this kind of state modeling. They are type-safe, database-friendly, and much cleaner than arbitrary strings spread across the codebase.&lt;/p&gt;

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

&lt;p&gt;This article explores where Enums work well, where they start breaking down, and how the State Pattern can help without introducing unnecessary complexity or framework-heavy abstractions.&lt;/p&gt;

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

&lt;p&gt;But over time, workflows tend to evolve. A cancellation process may eventually require refunding payments, restocking inventory, notifying external systems, creating audit logs, or dispatching events.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;p&gt;This is where applications experience state explosion—the point where transitions and side effects become increasingly difficult to isolate and reason about. At this stage, the code may still look “short,” but it is no longer simple.&lt;/p&gt;

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

&lt;p&gt;The State Pattern addresses this by moving behavior into dedicated state objects. Instead of one large conditional structure, each state becomes responsible for its own transitions and rules.&lt;/p&gt;

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;p&gt;One of the biggest advantages of state objects is test isolation. Instead of testing a large Enum with multiple branches and heavy mocking setups, each workflow state can be verified independently:&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;p&gt;The real problem starts when business workflows evolve and a single Enum begins accumulating infrastructure dependencies, transition orchestration, side effects, and validation logic. At that point, the issue is no longer code length—it is &lt;strong&gt;responsibility density&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The State Pattern is valuable not because it is “more advanced,” but because it isolates change. And in long-lived systems, isolated change is usually what keeps complexity manageable.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>development</category>
      <category>software</category>
      <category>php</category>
    </item>
    <item>
      <title>Trunk-Based Development: From Chaos to Flow</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 19 May 2026 14:16:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/trunk-based-development-from-chaos-to-flow-4mkg</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/trunk-based-development-from-chaos-to-flow-4mkg</guid>
      <description>&lt;p&gt;If you’ve followed the first two parts of this series, you know the hard truth: most teams aren’t actually doing Trunk-Based Development. They are doing “Short-lived Feature Branching” with better branding. We’ve talked about why your PRs are still too big and why the 6-month pull request is a parallel universe that kills delivery.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;Action: Implement real-time logging and alerting. If you merge a small change and the error rate spikes, you should know within seconds, not after a customer support ticket arrives.&lt;/li&gt;
&lt;/ul&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;&lt;strong&gt;The Strategy:&lt;/strong&gt; Slice vertically. One PR should deliver a minimal end-to-end functionality (even if hidden by a flag), followed by another PR that extends that behavior.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>software</category>
      <category>devops</category>
    </item>
    <item>
      <title>How Mutation Testing Exposes the Truth (PHP 2026 Edition)</title>
      <dc:creator>CodeCraft Diary</dc:creator>
      <pubDate>Tue, 12 May 2026 13:50:00 +0000</pubDate>
      <link>https://dev.to/codecraft_diary_3d13677fb/how-mutation-testing-exposes-the-truth-php-2026-edition-1m1l</link>
      <guid>https://dev.to/codecraft_diary_3d13677fb/how-mutation-testing-exposes-the-truth-php-2026-edition-1m1l</guid>
      <description>&lt;p&gt;You've got 85% code coverage. Your CI pipeline is green. You ship to production — and things break in ways your tests never caught. Sound familiar?&lt;/p&gt;

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

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




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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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




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

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

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

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

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




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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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




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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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




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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "$schema": "vendor/infection/infection/resources/schema.json",
    "source": {
        "directories": ["src"],
        "excludes": ["src/Infrastructure/Migrations"]
    },
    "mutators": {
        "@default": true
    },
    "testFramework": "phpunit",
    "testFrameworkOptions": "--testsuite=unit",
    "minMsi": 85,
    "minCoveredMsi": 90,
    "threads": 4,
    "logs": {
        "text": "var/log/infection.log",
        "html": "var/log/infection.html",
        "summary": "var/log/infection-summary.log"
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;minMsi&lt;/code&gt; and &lt;code&gt;minCoveredMsi&lt;/code&gt; thresholds are important — they let your CI pipeline fail if mutation score drops below acceptable levels, the same way PHPUnit can fail below a coverage threshold.&lt;/p&gt;




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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mutation-testing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup PHP&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shivammathur/setup-php@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;php-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8.3'&lt;/span&gt;
        &lt;span class="na"&gt;coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;xdebug&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer install --no-interaction&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Infection&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./vendor/bin/infection --threads=4 --min-msi=85 --min-covered-msi=90&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important note: Infection requires a coverage driver (Xdebug or PCOV) to know which mutants are relevant to which tests. PCOV is faster for large codebases; Xdebug gives more detail. I use Xdebug locally and PCOV in CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Objections — And Honest Answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"It's too slow."&lt;/strong&gt; It can be on large codebases, but &lt;code&gt;--threads&lt;/code&gt; and configuring &lt;code&gt;source.excludes&lt;/code&gt; to skip generated code, migrations, and DTOs makes a huge difference. I typically exclude everything that has no business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The MSI is too low to be useful."&lt;/strong&gt; Start with &lt;code&gt;--min-msi=0&lt;/code&gt; and just look at the HTML report. Prioritize killing mutants in your core domain logic first — that's where bugs actually hurt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It produces too many surviving mutants."&lt;/strong&gt; Some mutants are genuinely equivalent (they don't change behavior). Infection lets you mark these as ignored in config. Over time your noise floor drops significantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What My Workflow Looks Like in 2026
&lt;/h2&gt;

&lt;p&gt;My current approach on active PHP projects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PHPUnit with strict coverage&lt;/strong&gt; for the fast feedback loop during development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infection on every PR&lt;/strong&gt; targeting only changed files — using &lt;code&gt;--git-diff-filter&lt;/code&gt; (available since Infection 0.27) to keep CI times reasonable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Infection run weekly&lt;/strong&gt; on the &lt;code&gt;main&lt;/code&gt; branch to catch gradual MSI drift.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;--git-diff-filter&lt;/code&gt; flag is a game-changer for larger repos — it only mutates code touched in the current diff, so mutation testing stays practical even on monorepos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./vendor/bin/infection &lt;span class="nt"&gt;--git-diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AM &lt;span class="nt"&gt;--threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;Code coverage is a floor, not a ceiling. It tells you the minimum — which lines were touched. Mutation testing tells you something far more valuable: whether those lines are &lt;em&gt;protected&lt;/em&gt; by tests that would actually catch a regression.&lt;/p&gt;

&lt;p&gt;If you're publishing technical content in 2026 and you're not talking about mutation testing, you're leaving one of PHP's most powerful quality tools completely off the table. The tooling has matured, the CI integration is straightforward, and the payoff in confidence is real.&lt;/p&gt;

&lt;p&gt;Start with a single service class. Run Infection. Look at what survives. I promise you'll find something surprising.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://infection.github.io/guide/" rel="noopener noreferrer"&gt;Infection PHP documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/infection/infection" rel="noopener noreferrer"&gt;Infection GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://phpunit.de/" rel="noopener noreferrer"&gt;PHPUnit documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>php</category>
      <category>software</category>
    </item>
    <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>
  </channel>
</rss>
