<?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: Saqueib Ansari</title>
    <description>The latest articles on DEV Community by Saqueib Ansari (@saqueib).</description>
    <link>https://dev.to/saqueib</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%2F3826808%2Fe6a01e4e-75be-4474-bfb1-87c09122c718.jpeg</url>
      <title>DEV Community: Saqueib Ansari</title>
      <link>https://dev.to/saqueib</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saqueib"/>
    <language>en</language>
    <item>
      <title>Benchmarking Filament Admin Screens Without Guessing</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Fri, 28 Aug 2026 06:11:59 +0000</pubDate>
      <link>https://dev.to/saqueib/benchmarking-filament-admin-screens-without-guessing-ani</link>
      <guid>https://dev.to/saqueib/benchmarking-filament-admin-screens-without-guessing-ani</guid>
      <description>&lt;p&gt;Slow Filament screens are usually blamed on the wrong layer. Teams upgrade packages, swap columns, or remove filters before they know whether the real cost is database work, Livewire hydration, authorization checks, or table configuration. That is how you waste a week and still keep a slow screen.&lt;/p&gt;

&lt;p&gt;The better approach is simple: &lt;strong&gt;benchmark one screen, break the cost into parts, then change the part that is actually expensive&lt;/strong&gt;. Filament sits on top of Laravel and Livewire, so a slow admin page is rarely a single-problem system. You need a workflow that makes the bottleneck obvious before you touch the UI.&lt;/p&gt;

&lt;p&gt;This tutorial is that workflow. It is practical, repeatable, and biased toward real codebases where the screen already exists and users already think it feels slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one repeatable benchmark
&lt;/h2&gt;

&lt;p&gt;Do not begin with your whole panel. Pick one screen that people actually complain about: a resource index with heavy filters, an edit page with relation managers, or a dashboard that loads too many widgets at once.&lt;/p&gt;

&lt;p&gt;Then make the benchmark repeatable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the same local database snapshot each run&lt;/li&gt;
&lt;li&gt;test with a realistic row count, not ten seed records&lt;/li&gt;
&lt;li&gt;measure a cold page load and one warm interaction&lt;/li&gt;
&lt;li&gt;close unrelated browser tabs and disable noisy extensions&lt;/li&gt;
&lt;li&gt;write the numbers down before changing code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most Filament screens, I want three checkpoints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;initial page load&lt;/li&gt;
&lt;li&gt;a common interaction like search, filter, or sort&lt;/li&gt;
&lt;li&gt;opening a row action or modal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you do not measure those separately, you will blur together very different costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to capture
&lt;/h3&gt;

&lt;p&gt;At minimum, record these numbers for each checkpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total request time&lt;/li&gt;
&lt;li&gt;SQL query count&lt;/li&gt;
&lt;li&gt;total SQL time&lt;/li&gt;
&lt;li&gt;response payload size&lt;/li&gt;
&lt;li&gt;number of Livewire requests triggered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That already tells you a lot. A page with 220 queries and 900 ms of SQL time is a database problem until proven otherwise. A page with modest SQL time but huge payloads and repeated requests is usually a Livewire or component-state problem. A page with acceptable backend timings but sluggish interactions may be rendering too much UI or doing too much per row.&lt;/p&gt;

&lt;p&gt;A lightweight way to start is adding &lt;code&gt;Server-Timing&lt;/code&gt; and query metrics to local requests:&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;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Middleware&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;Closure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Events\QueryExecuted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&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;Symfony\Component\HttpFoundation\Response&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;ProfileAdminRequest&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;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Response&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="nf"&gt;str_starts_with&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;path&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="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&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;$queryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$queryTimeMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;QueryExecuted&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$queryCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$queryTimeMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$queryCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$queryTimeMs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="cd"&gt;/** @var Response $response */&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;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$totalMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$startedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&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="n"&gt;headers&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;'Server-Timing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app;dur=%.1f, sql;dur=%.1f, queries;desc="%d"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$totalMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$queryTimeMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$queryCount&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;$response&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 Chrome DevTools gives you backend timing hints without turning every request into a detective story.&lt;/p&gt;

&lt;p&gt;If you want richer local visibility, Laravel &lt;a href="https://laravel.com/docs/13.x/telescope" rel="noopener noreferrer"&gt;Telescope&lt;/a&gt; is still one of the most useful ways to inspect queries, requests, jobs, and exceptions in one place. For Filament-specific debugging, it is often enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add instrumentation where Filament actually spends time
&lt;/h2&gt;

&lt;p&gt;A Filament admin page usually burns time in four places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database queries&lt;/li&gt;
&lt;li&gt;per-record authorization or visibility logic&lt;/li&gt;
&lt;li&gt;Livewire hydration and payload size&lt;/li&gt;
&lt;li&gt;table or form configuration that does more work than the user needs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means your benchmark has to go beyond total duration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database timing first
&lt;/h3&gt;

&lt;p&gt;Laravel already gives you enough primitives to catch obvious waste. In addition to &lt;code&gt;DB::listen&lt;/code&gt;, use &lt;code&gt;whenQueryingForLongerThan&lt;/code&gt; in local or staging to flag suspicious requests early:&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;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Providers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Connection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Events\QueryExecuted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Log&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;AppServiceProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ServiceProvider&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;boot&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="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whenQueryingForLongerThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Connection&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;QueryExecuted&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;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Slow query threshold exceeded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'sql'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'time_ms'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'connection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getName&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That will not replace profiling, but it quickly exposes screens where a table interaction is quietly executing a pile of avoidable work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch request shape, not just runtime
&lt;/h3&gt;

&lt;p&gt;Livewire pages can feel slow even when SQL looks fine. Two common reasons are oversized component state and too many requests for small UI changes.&lt;/p&gt;

&lt;p&gt;Livewire’s current docs explicitly warn that storing large Eloquent collections as component properties can hurt performance because hydration re-executes work on subsequent requests. That is why a Filament page with "helpful" preloaded collections often degrades over time instead of getting better.&lt;/p&gt;

&lt;p&gt;Also watch how many requests a single interaction triggers. Search fields, reactive filters, dependent selects, and polling widgets can stack together in ugly ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate initial render from interaction cost
&lt;/h3&gt;

&lt;p&gt;This is where teams often misdiagnose the problem.&lt;/p&gt;

&lt;p&gt;A screen may load fine at first, then feel terrible when users search or change filters. That usually points to table search, per-row closures, or repeated Livewire updates. Filament’s table docs note that global search term splitting can hurt performance on large datasets, and that is exactly the kind of detail that matters only after you have measured the slow interaction path.&lt;/p&gt;

&lt;p&gt;So benchmark both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;first render of the page&lt;/li&gt;
&lt;li&gt;first search request&lt;/li&gt;
&lt;li&gt;first filter request&lt;/li&gt;
&lt;li&gt;first modal open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not assume the same fix helps all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the usual bottlenecks correctly
&lt;/h2&gt;

&lt;p&gt;Once you have timings, the next step is classification. Most slow Filament screens fall into a few predictable buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database-heavy tables
&lt;/h3&gt;

&lt;p&gt;This is the classic case: relation columns, badge counts, computed summaries, and searchable fields all pile onto one index page. The query count spikes, SQL time dominates the request, and every sort or filter makes it worse.&lt;/p&gt;

&lt;p&gt;Typical causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing eager loading for related columns&lt;/li&gt;
&lt;li&gt;per-row &lt;code&gt;count()&lt;/code&gt; or aggregate calls inside closures&lt;/li&gt;
&lt;li&gt;global search across too many text columns&lt;/li&gt;
&lt;li&gt;filters that build expensive subqueries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;options()&lt;/code&gt; lists loading huge tables on every request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where Filament table configuration matters more than package upgrades. If a table is asking the database to do the wrong work, a faster Filament release will only make the wrong work happen a bit more efficiently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization-heavy rows and actions
&lt;/h3&gt;

&lt;p&gt;Filament makes it easy to add &lt;code&gt;visible()&lt;/code&gt;, &lt;code&gt;hidden()&lt;/code&gt;, &lt;code&gt;disabled()&lt;/code&gt;, and policy-based actions everywhere. That is good ergonomically and dangerous operationally.&lt;/p&gt;

&lt;p&gt;If each row action or badge checks permissions through expensive closures, you can end up paying authorization cost hundreds of times on one table render. The page still looks like a "UI problem," but the real issue is repeated decision logic.&lt;/p&gt;

&lt;p&gt;A useful smell test is this: if reducing page size from 50 records to 10 dramatically improves the screen even when SQL is already reasonable, per-row logic is probably part of the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Livewire state and request churn
&lt;/h3&gt;

&lt;p&gt;A different pattern is moderate SQL time but clumsy interactions. Search feels sticky. Filter changes trigger multiple requests. Opening a modal rehydrates more state than expected.&lt;/p&gt;

&lt;p&gt;That usually means too much data lives in the component, too many fields are reactive by default, or several components are bundled into one slow request path.&lt;/p&gt;

&lt;p&gt;Livewire gives you tools here, but they are not magic. &lt;code&gt;#[Isolate]&lt;/code&gt; helps when one component is expensive and independent. Lazy loading helps when widgets do not need to block first paint. Computed properties help when you were previously carrying around heavy collections as public state. The point is to shrink work per interaction, not to sprinkle attributes at random.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production-only slowness
&lt;/h3&gt;

&lt;p&gt;If local numbers look acceptable and production feels worse, stop tuning the screen first and check deployment posture.&lt;/p&gt;

&lt;p&gt;Filament’s deployment docs recommend &lt;code&gt;php artisan filament:optimize&lt;/code&gt;, which wraps component and icon caching, and Laravel still benefits from the usual production optimizations like &lt;code&gt;php artisan optimize&lt;/code&gt; and correct OPcache setup. Those are not substitutes for bad screen design, but they absolutely matter once the screen is otherwise healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix the bottleneck you measured
&lt;/h2&gt;

&lt;p&gt;This is the part that should feel boring. Good performance work is usually a series of targeted, unsurprising fixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tighten the table query before touching the UI
&lt;/h3&gt;

&lt;p&gt;If the benchmark says the table query is the bottleneck, change the query first:&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;Filament\Tables&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;Filament\Tables\Table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="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;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Table&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Table&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;$table&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;modifyQueryUsing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;select&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;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'customer:id,name'&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;withCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'items'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="nc"&gt;Tables\Columns\TextColumn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'customer.name'&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;searchable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;Tables\Columns\TextColumn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'items_count'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Tables\Columns\TextColumn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;badge&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;Tables\Columns\TextColumn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'total'&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;money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Tables\Columns\TextColumn&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dateTime&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;defaultPaginationPageOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&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;splitSearchTerms&lt;/span&gt;&lt;span class="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things are happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the base select is narrower&lt;/li&gt;
&lt;li&gt;relations and counts are loaded intentionally&lt;/li&gt;
&lt;li&gt;search behavior is made cheaper for large datasets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a much better starting point than deleting columns until the page stops hurting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stop doing expensive work per row
&lt;/h3&gt;

&lt;p&gt;If per-record closures are the issue, move repeated logic up a level.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;action visibility closures that hit services or policies for every record&lt;/li&gt;
&lt;li&gt;badge or description closures that trigger relation access lazily&lt;/li&gt;
&lt;li&gt;ad hoc formatting that performs database lookups inside the column callback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Better pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preload the data the UI needs&lt;/li&gt;
&lt;li&gt;compute coarse permissions once per request where possible&lt;/li&gt;
&lt;li&gt;reserve per-row checks for cases that are truly record-specific&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also where reducing visual ambition helps. Admin tables do not need to be miniature dashboards. If each row shows three badges, two counts, a derived status, and four action buttons, you are paying for that complexity every render.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shrink Livewire work on interaction
&lt;/h3&gt;

&lt;p&gt;When interactions are the main problem, focus on state and request frequency.&lt;/p&gt;

&lt;p&gt;Useful moves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replace large public collections with computed lookups or paginated queries&lt;/li&gt;
&lt;li&gt;avoid making fields reactive unless the user genuinely needs instant feedback&lt;/li&gt;
&lt;li&gt;lazy-load widgets that are not needed for first paint&lt;/li&gt;
&lt;li&gt;isolate an expensive widget only if it does not need to coordinate with the rest of the page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be careful here: request bundling in Livewire often helps overall performance. Isolating components is for targeted cases, not a default style.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use upgrades as a measured final step
&lt;/h3&gt;

&lt;p&gt;Filament and Livewire releases do ship real performance work. Newer versions have improved rendering paths, component handling, and request behavior. But the right order is still:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;measure the screen&lt;/li&gt;
&lt;li&gt;fix obvious query and state issues&lt;/li&gt;
&lt;li&gt;upgrade if the version gap is meaningful&lt;/li&gt;
&lt;li&gt;rerun the same benchmark&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Otherwise you never learn whether the upgrade solved your problem or merely shifted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn one benchmark into a guardrail
&lt;/h2&gt;

&lt;p&gt;A single successful tuning pass is useful. A repeatable performance habit is better.&lt;/p&gt;

&lt;p&gt;Once you make one Filament screen faster, keep the process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store the before and after timings in the PR description&lt;/li&gt;
&lt;li&gt;keep one realistic local dataset for admin profiling&lt;/li&gt;
&lt;li&gt;add a short checklist for new heavy resources&lt;/li&gt;
&lt;li&gt;rerun the same benchmark after package upgrades&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A checklist can be brutally small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;does the table eager load displayed relations?&lt;/li&gt;
&lt;li&gt;are counts and aggregates precomputed sensibly?&lt;/li&gt;
&lt;li&gt;is search scoped narrowly enough?&lt;/li&gt;
&lt;li&gt;are actions or badges doing expensive per-row work?&lt;/li&gt;
&lt;li&gt;is Livewire carrying more state than the screen needs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is enough to stop most regressions.&lt;/p&gt;

&lt;p&gt;If you want useful official references while working through this, keep these close:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filament table docs: &lt;a href="https://filamentphp.com/docs/5.x/tables/overview" rel="noopener noreferrer"&gt;filamentphp.com/docs/5.x/tables/overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Filament deployment and optimization: &lt;a href="https://filamentphp.com/docs/4.x/deployment" rel="noopener noreferrer"&gt;filamentphp.com/docs/4.x/deployment&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Livewire properties and hydration guidance: &lt;a href="https://livewire.laravel.com/docs/4.x/properties" rel="noopener noreferrer"&gt;livewire.laravel.com/docs/4.x/properties&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Livewire isolation for expensive independent components: &lt;a href="https://livewire.laravel.com/docs/4.x/attribute-isolate" rel="noopener noreferrer"&gt;livewire.laravel.com/docs/4.x/attribute-isolate&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Laravel Telescope: &lt;a href="https://laravel.com/docs/13.x/telescope" rel="noopener noreferrer"&gt;laravel.com/docs/13.x/telescope&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical rule is simple: &lt;strong&gt;do not optimize Filament screens by taste&lt;/strong&gt;. Measure one screen, separate SQL from hydration from UI logic, fix the dominant cost, and rerun the same benchmark. That is how you make an admin panel faster without guessing.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/benchmark-filament-admin-screens-without-guesswork/" rel="noopener noreferrer"&gt;https://qcode.in/benchmark-filament-admin-screens-without-guesswork/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>livewire</category>
      <category>performance</category>
    </item>
    <item>
      <title>When Laravel Checkout Flows Outgrow Job Chains</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Tue, 25 Aug 2026 04:52:32 +0000</pubDate>
      <link>https://dev.to/saqueib/when-laravel-checkout-flows-outgrow-job-chains-5elb</link>
      <guid>https://dev.to/saqueib/when-laravel-checkout-flows-outgrow-job-chains-5elb</guid>
      <description>&lt;p&gt;Most Laravel teams start checkout flows with job chains because they feel clean, native, and cheap to ship. That works right up until checkout stops being one thing. The moment payment capture, inventory reservation, license provisioning, welcome email, analytics, and rollback logic start pulling on each other, a plain chain turns into a fragile story told across too many queue jobs.&lt;/p&gt;

&lt;p&gt;The practical recommendation is simple: &lt;strong&gt;use job chains for short, mostly linear flows with low rollback cost. Use saga-style workflows when your checkout crosses service boundaries, needs compensation, or must survive partial success without guessing what happened.&lt;/strong&gt; If your team is debating this too late, that usually means the chain already outgrew its shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job Chains Win Early Because They Hide Complexity
&lt;/h2&gt;

&lt;p&gt;Laravel job chains are attractive for good reasons. They are built into the framework, easy to read, and good enough for a surprising amount of business logic. For a simple paid flow, a chain can express the happy path clearly:&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;chain&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;ChargeCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$checkoutId&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;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$checkoutId&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;SendReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$checkoutId&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;dispatch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is hard to argue with. The intent is obvious. The code stays close to Laravel’s queue model. You do not need extra infrastructure, workflow state, or orchestration concepts just to move money and send an email.&lt;/p&gt;

&lt;p&gt;This approach stays healthy when three conditions hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The steps are mostly linear.&lt;/li&gt;
&lt;li&gt;Failure handling is local, not cross-cutting.&lt;/li&gt;
&lt;li&gt;A failed step can usually be retried without inventing rollback semantics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters more than most teams admit. Retry is easy when the side effect is harmless. Retry is dangerous when the job already captured a payment, already created a subscription, or already called a third-party API with no clean undo.&lt;/p&gt;

&lt;p&gt;The hidden cost of job chains is that they make the happy path obvious and the failure path implicit. Early on, that feels efficient. Later, it becomes the reason the system is hard to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Checkout Flows Break the Job Chain Model
&lt;/h2&gt;

&lt;p&gt;Checkout is not one transaction. It is a collection of side effects pretending to be one transaction.&lt;/p&gt;

&lt;p&gt;A realistic flow might do all of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authorize or capture payment&lt;/li&gt;
&lt;li&gt;create the order record&lt;/li&gt;
&lt;li&gt;allocate inventory or credits&lt;/li&gt;
&lt;li&gt;provision a workspace or subscription&lt;/li&gt;
&lt;li&gt;send email&lt;/li&gt;
&lt;li&gt;emit events to analytics and CRM&lt;/li&gt;
&lt;li&gt;roll back selected steps if something later fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A job chain can execute those steps in order, but it does not give you a strong model for &lt;strong&gt;state transitions&lt;/strong&gt;, &lt;strong&gt;partial completion&lt;/strong&gt;, or &lt;strong&gt;compensation&lt;/strong&gt;. Once you care about those things, your chain logic usually leaks into several places at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first failure mode: side effects succeed before the chain fails
&lt;/h3&gt;

&lt;p&gt;Suppose payment succeeds, order creation succeeds, but workspace provisioning fails because the downstream service is degraded. A plain chain can stop, retry, or send an alert. What it cannot do cleanly is answer the business question: &lt;strong&gt;what should the system do now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you refund immediately, you may create duplicate refunds under retries unless you designed idempotency correctly.&lt;br&gt;
If you do nothing, the customer may be charged without receiving the product.&lt;br&gt;
If you retry indefinitely, support inherits the ambiguity.&lt;/p&gt;

&lt;p&gt;The problem is not that Laravel chains are bad. The problem is that the chain abstraction is too thin for workflows that need explicit recovery policy.&lt;/p&gt;
&lt;h3&gt;
  
  
  The second failure mode: rollback gets scattered
&lt;/h3&gt;

&lt;p&gt;Teams usually patch this with ad hoc rollback code inside &lt;code&gt;failed()&lt;/code&gt; handlers, listeners, or follow-up jobs:&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;ProvisionWorkspace&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;Provisioner&lt;/span&gt; &lt;span class="nv"&gt;$provisioner&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;$provisioner&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createForCheckout&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;checkoutId&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;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;RefundPayment&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;checkoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;ReleaseReservedCredits&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;checkoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;MarkCheckoutForReview&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;checkoutId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&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 where a chain starts lying to you. The flow is no longer linear. Recovery is now distributed across job classes, queue retries, manual support rules, and sometimes cron-based cleanup. You still have “a chain,” but operationally you have a workflow engine you built by accident.&lt;/p&gt;

&lt;h3&gt;
  
  
  The third failure mode: nobody can answer “what state is this checkout in?”
&lt;/h3&gt;

&lt;p&gt;This is the big one. During incidents, the real question is rarely “did job X fail?” It is usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was the customer charged?&lt;/li&gt;
&lt;li&gt;Was the subscription created?&lt;/li&gt;
&lt;li&gt;Should we retry, compensate, or wait?&lt;/li&gt;
&lt;li&gt;Is this safe to replay?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Job chains are execution plumbing. They are not a great domain model for these answers unless you add a lot of extra state tracking around them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Saga-Style Workflows Actually Buy You
&lt;/h2&gt;

&lt;p&gt;A saga-style workflow is not magic. It is just a more honest model for long-running, multi-step processes with compensating actions.&lt;/p&gt;

&lt;p&gt;Instead of treating the flow as “run these jobs in order,” you treat it as &lt;strong&gt;a durable state machine with explicit forward steps and explicit undo behavior&lt;/strong&gt;. Each step knows what success means, what failure means, and whether compensation is required.&lt;/p&gt;

&lt;p&gt;That changes the design conversation in useful ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  You stop pretending rollback is a database transaction
&lt;/h3&gt;

&lt;p&gt;A checkout flow involving Stripe, email, provisioning, and internal records is not one atomic transaction. A saga accepts that reality. Each step commits independently, and the workflow defines what to do if a later step fails.&lt;/p&gt;

&lt;p&gt;Typical compensations look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reverse payment capture or issue refund&lt;/li&gt;
&lt;li&gt;release inventory or reserved credits&lt;/li&gt;
&lt;li&gt;disable a partially created subscription&lt;/li&gt;
&lt;li&gt;cancel downstream provisioning requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much cleaner than burying recovery behavior in random &lt;code&gt;failed()&lt;/code&gt; methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  You get durable workflow state
&lt;/h3&gt;

&lt;p&gt;A proper saga keeps state like &lt;code&gt;pending_payment&lt;/code&gt;, &lt;code&gt;payment_captured&lt;/code&gt;, &lt;code&gt;subscription_provisioned&lt;/code&gt;, &lt;code&gt;compensating&lt;/code&gt;, or &lt;code&gt;completed&lt;/code&gt;. That sounds boring until a production issue hits. Then it becomes the difference between support guessing and support knowing.&lt;/p&gt;

&lt;p&gt;A minimal Laravel-oriented representation might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutWorkflowData&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;string&lt;/span&gt; &lt;span class="nv"&gt;$checkoutId&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;?string&lt;/span&gt; &lt;span class="nv"&gt;$paymentIntentId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$subscriptionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'started'&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;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutSaga&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;CheckoutWorkflowData&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;paymentIntentId&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;capturePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$data&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="s1"&gt;'payment_captured'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscriptionId&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;provisionSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$data&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="s1"&gt;'subscription_provisioned'&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;sendReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$data&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="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$data&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="s1"&gt;'compensating'&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;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscriptionId&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;cancelSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;subscriptionId&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;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;paymentIntentId&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;refundPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;paymentIntentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&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="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="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 simplified, but the shape is already better. The system now has a memory of what happened, not just a queue of what was supposed to happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  You can make retry policy step-specific
&lt;/h3&gt;

&lt;p&gt;Not every failure deserves the same response. Email can usually retry. Payment capture needs idempotency and stricter safeguards. Provisioning may need bounded retries before compensation kicks in.&lt;/p&gt;

&lt;p&gt;Saga-style workflows let you say that clearly. That is the real value: &lt;strong&gt;not more abstraction, but better failure semantics.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Right Choice Depends on Your Failure Cost
&lt;/h2&gt;

&lt;p&gt;This is where teams should be more opinionated.&lt;/p&gt;

&lt;p&gt;If your checkout flow is a single application writing to one database and sending one or two non-critical side effects, job chains are usually the correct choice. Adding a workflow layer too early is architecture cosplay.&lt;/p&gt;

&lt;p&gt;If your checkout touches money, entitlements, and third-party systems, the question is no longer “can a chain run this?” The question is “what does failure mean, and can we explain recovery without hand-waving?”&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose job chains when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the flow has 3-5 mostly linear steps&lt;/li&gt;
&lt;li&gt;failures are either retryable or cheap to repair manually&lt;/li&gt;
&lt;li&gt;no step requires formal compensation beyond simple cleanup&lt;/li&gt;
&lt;li&gt;support rarely needs a per-checkout execution timeline&lt;/li&gt;
&lt;li&gt;idempotency is straightforward and already enforced&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Choose saga workflows when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;payment and product delivery can diverge&lt;/li&gt;
&lt;li&gt;rollback spans multiple services or vendors&lt;/li&gt;
&lt;li&gt;some steps are slow, asynchronous, or externally acknowledged later&lt;/li&gt;
&lt;li&gt;support and ops need workflow state, not just failed jobs&lt;/li&gt;
&lt;li&gt;duplicate execution would be expensive or embarrassing&lt;/li&gt;
&lt;li&gt;you are already writing custom recovery logic in multiple places&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last bullet is the tell. If your team has a spreadsheet of “what to do when step 4 fails after step 2 succeeded,” you do not have a simple chain anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Laravel Team’s Migration Path Should Be Boring
&lt;/h2&gt;

&lt;p&gt;The mistake is treating this as a binary rewrite. You do not need to replace every queue flow with a grand workflow platform. Start with the one path that hurts: usually paid checkout, subscription activation, or account onboarding with entitlements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: keep jobs, add explicit workflow state
&lt;/h3&gt;

&lt;p&gt;Before adopting a full saga engine, add a workflow record that tracks state transitions explicitly. Even if the execution still uses jobs under the hood, this gives you observability and a stable place for decision logic.&lt;/p&gt;

&lt;p&gt;At minimum, track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;workflow id or checkout id&lt;/li&gt;
&lt;li&gt;current status&lt;/li&gt;
&lt;li&gt;completed steps&lt;/li&gt;
&lt;li&gt;compensations run&lt;/li&gt;
&lt;li&gt;last error&lt;/li&gt;
&lt;li&gt;next retry time or terminal disposition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This alone will improve incident handling more than many teams expect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: pull compensation out of &lt;code&gt;failed()&lt;/code&gt; handlers
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;failed()&lt;/code&gt; is fine for local cleanup. It is a poor home for business-critical compensation policy. Move rollback decisions into an orchestration layer where the whole workflow state is visible.&lt;/p&gt;

&lt;p&gt;That can still be Laravel-native. You do not need to import complexity just to become explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: adopt a durable workflow runtime only where it pays rent
&lt;/h3&gt;

&lt;p&gt;If the flow becomes long-running or highly asynchronous, a durable workflow system starts earning its cost. That might be an internal saga package, a Laravel-oriented workflow library, or an external engine if your scale and complexity justify it.&lt;/p&gt;

&lt;p&gt;The evaluation criteria should be boring and practical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can it persist workflow state durably?&lt;/li&gt;
&lt;li&gt;Can it replay safely?&lt;/li&gt;
&lt;li&gt;Can it model compensation cleanly?&lt;/li&gt;
&lt;li&gt;Can support inspect execution without reading queue internals?&lt;/li&gt;
&lt;li&gt;Can developers test failure branches without building rituals?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official Laravel queue docs are still the baseline here: &lt;a href="https://laravel.com/docs/queues" rel="noopener noreferrer"&gt;https://laravel.com/docs/queues&lt;/a&gt;. Read them first, then decide whether you need more than queues.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Tradeoff Is Cognitive Load, Not Just Infrastructure
&lt;/h2&gt;

&lt;p&gt;Sagas are better for complex checkout flows, but they are not free. They introduce workflow concepts, explicit state modeling, and more up-front design work. That is worthwhile only when the business flow is already complex enough to deserve it.&lt;/p&gt;

&lt;p&gt;A plain job chain keeps code smaller when the domain is simple. A saga keeps incidents smaller when the domain is not.&lt;/p&gt;

&lt;p&gt;That is the comparison that matters.&lt;/p&gt;

&lt;p&gt;The wrong move is staying on job chains because they are familiar after the workflow has already become compensation-heavy. The second wrong move is adopting a heavyweight workflow abstraction before the team has a real failure-handling problem.&lt;/p&gt;

&lt;p&gt;My rule of thumb is blunt: &lt;strong&gt;if a failed checkout can leave money captured but value undelivered, model the flow as a saga. If failure mostly means “retry this later,” keep the chain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That one decision line will save most Laravel teams from both under-engineering and workflow theater.&lt;/p&gt;

&lt;p&gt;For adjacent reading, Laravel’s bus and queue primitives remain useful even in a saga-oriented design because jobs still make good execution units. The difference is that they stop being the source of truth for the business process.&lt;/p&gt;

&lt;p&gt;Build the chain first when the flow is genuinely small. The moment recovery becomes a first-class requirement, stop stretching the chain abstraction past its limit and promote the flow into a workflow with explicit state and compensation.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/saga-workflows-vs-job-chains-laravel-checkout-flows/" rel="noopener noreferrer"&gt;https://qcode.in/saga-workflows-vs-job-chains-laravel-checkout-flows/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>queues</category>
      <category>workflows</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A Better AI Learning Workflow for Engineers Who Need Shipping Context</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Sat, 22 Aug 2026 05:21:18 +0000</pubDate>
      <link>https://dev.to/saqueib/a-better-ai-learning-workflow-for-engineers-who-need-shipping-context-408l</link>
      <guid>https://dev.to/saqueib/a-better-ai-learning-workflow-for-engineers-who-need-shipping-context-408l</guid>
      <description>&lt;p&gt;Most engineers do not have a learning problem. They have a &lt;strong&gt;translation problem&lt;/strong&gt;. An LLM can explain a concept in five seconds, but that does not tell you what breaks in your codebase, what tradeoff matters under real traffic, or what test you should write before merging.&lt;/p&gt;

&lt;p&gt;If your goal is to ship, the right AI learning workflow is not summary-first. It is &lt;strong&gt;decision-first&lt;/strong&gt;. You use the model to compress research into implementation pressure: constraints, failure modes, interface changes, migration risk, and testable claims. That is the difference between "I understand the topic" and "I can make the next production decision with confidence."&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With Shipping Questions, Not Topic Questions
&lt;/h2&gt;

&lt;p&gt;A weak workflow starts with prompts like "teach me X" or "summarize Y." That creates polished understanding and poor execution. You get vocabulary, broad concepts, and clean explanations, but not the details that actually move a codebase forward.&lt;/p&gt;

&lt;p&gt;A stronger workflow starts by anchoring the learning to a concrete engineering surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a feature you need to build&lt;/li&gt;
&lt;li&gt;a system you need to change&lt;/li&gt;
&lt;li&gt;a bug class you need to eliminate&lt;/li&gt;
&lt;li&gt;an architectural choice you need to make&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That changes the prompt shape immediately. Instead of asking what a concept is, ask where it will collide with your system.&lt;/p&gt;

&lt;p&gt;For example, if you are learning retrieval pipelines, the useful question is not "what is RAG?" It is: &lt;strong&gt;where does retrieval introduce latency, stale context, ranking failure, and observability debt in my stack?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are learning background job orchestration, the useful question is not "how do queues work?" It is: &lt;strong&gt;what consistency guarantees do I actually need, and what happens when retries meet non-idempotent side effects?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This framing matters because most implementation failures come from the edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;state transitions&lt;/li&gt;
&lt;li&gt;partial failures&lt;/li&gt;
&lt;li&gt;schema drift&lt;/li&gt;
&lt;li&gt;rate limits&lt;/li&gt;
&lt;li&gt;concurrency&lt;/li&gt;
&lt;li&gt;human maintenance cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LLMs are much more useful when you force them to talk about those edges early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Notes Around Decisions, Not Facts
&lt;/h2&gt;

&lt;p&gt;The output of AI-assisted learning should not be a generic summary document. It should be a working note that helps you decide what to do next. That means your notes need a different structure.&lt;/p&gt;

&lt;p&gt;A good engineering note usually needs five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What problem are we actually solving?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What assumptions are true in our codebase?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What are the main options and their tradeoffs?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What can fail in production?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What should we implement, test, and monitor?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds obvious, but most AI-generated notes skip at least three of them. They overproduce explanation and underproduce operational guidance.&lt;/p&gt;

&lt;h3&gt;
  
  
  A better note template
&lt;/h3&gt;

&lt;p&gt;Use a template that forces implementation consequences to the surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Topic:
Current use case:
Decision we need to make:

What changes in code:
&lt;span class="p"&gt;-&lt;/span&gt; modules touched
&lt;span class="p"&gt;-&lt;/span&gt; new interfaces or abstractions
&lt;span class="p"&gt;-&lt;/span&gt; schema/config changes
&lt;span class="p"&gt;-&lt;/span&gt; deployment impact

Tradeoffs:
&lt;span class="p"&gt;-&lt;/span&gt; latency
&lt;span class="p"&gt;-&lt;/span&gt; cost
&lt;span class="p"&gt;-&lt;/span&gt; complexity
&lt;span class="p"&gt;-&lt;/span&gt; debuggability
&lt;span class="p"&gt;-&lt;/span&gt; vendor lock-in

Failure modes:
&lt;span class="p"&gt;-&lt;/span&gt; what breaks first
&lt;span class="p"&gt;-&lt;/span&gt; what is silent vs obvious
&lt;span class="p"&gt;-&lt;/span&gt; rollback strategy

Validation plan:
&lt;span class="p"&gt;-&lt;/span&gt; unit tests
&lt;span class="p"&gt;-&lt;/span&gt; integration tests
&lt;span class="p"&gt;-&lt;/span&gt; load checks
&lt;span class="p"&gt;-&lt;/span&gt; metrics/logging

Recommendation:
&lt;span class="p"&gt;-&lt;/span&gt; choose X because...
&lt;span class="p"&gt;-&lt;/span&gt; reject Y because...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure does two useful things. First, it turns the model into a design sparring partner instead of a summarizer. Second, it gives you a note that can evolve into an ADR, implementation ticket, or PR checklist.&lt;/p&gt;

&lt;p&gt;For engineers working in Laravel, Node, or mixed full-stack systems, this is especially valuable because the risk is rarely in the idea itself. The risk is in the seams between app code, background work, storage, and third-party APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow That Produces Shipping Context
&lt;/h2&gt;

&lt;p&gt;The practical workflow is simple. The discipline is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Gather raw material aggressively
&lt;/h3&gt;

&lt;p&gt;Pull in the docs, code snippets, architecture notes, issue threads, and your own current implementation context. If you are learning from a concept page or official docs, pair that with your local constraints immediately.&lt;/p&gt;

&lt;p&gt;Useful official references are usually the boring ones: framework docs, API docs, protocol specs, and vendor guides. For AI-heavy work, that often means starting with pages like the &lt;a href="https://platform.openai.com/docs" rel="noopener noreferrer"&gt;OpenAI docs&lt;/a&gt; or the &lt;a href="https://modelcontextprotocol.io/introduction" rel="noopener noreferrer"&gt;Model Context Protocol introduction&lt;/a&gt;, then forcing the model to map those ideas into your application boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Ask for contradictions and pressure points
&lt;/h3&gt;

&lt;p&gt;Do not ask the model to explain the material back to you. Ask it where the clean explanation stops being enough.&lt;/p&gt;

&lt;p&gt;Good prompts here sound like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I am evaluating this for a production app.
Given these constraints:
- Laravel API backend
- queue workers for async tasks
- per-request latency budget under 1.5s
- users can retry actions
- external model/API calls may fail or rate limit

Tell me:
1. which assumptions in the docs break first in production
2. what design mistakes engineers usually make on first implementation
3. what has to be idempotent
4. what should be synchronous vs queued
5. what should be measured from day one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prompt shape is powerful because it asks for &lt;strong&gt;stress&lt;/strong&gt;, not explanation. You are trying to extract where reality pushes back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Convert learning into code-level consequences
&lt;/h3&gt;

&lt;p&gt;This is where most people stop too early. They now "understand" the topic and move on. That is exactly where you should get more specific.&lt;/p&gt;

&lt;p&gt;Ask the model to translate the concept into concrete code impacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which classes or services likely change&lt;/li&gt;
&lt;li&gt;where boundaries should move&lt;/li&gt;
&lt;li&gt;what config needs to exist&lt;/li&gt;
&lt;li&gt;which invariants must hold&lt;/li&gt;
&lt;li&gt;what test matrix becomes necessary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if you are adopting AI-assisted content enrichment in a publishing pipeline, the real questions are not about prompting style. They are about job retries, duplicate writes, content review states, and auditability of generated output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Produce implementation artifacts immediately
&lt;/h3&gt;

&lt;p&gt;The best learning session ends with something your team could actually use. That can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an ADR draft&lt;/li&gt;
&lt;li&gt;a migration plan&lt;/li&gt;
&lt;li&gt;a risk register&lt;/li&gt;
&lt;li&gt;a PR breakdown&lt;/li&gt;
&lt;li&gt;a test plan&lt;/li&gt;
&lt;li&gt;an observability checklist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the conversation does not end in an artifact, it probably stayed too abstract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Learning an AI Feature the Wrong Way vs the Right Way
&lt;/h2&gt;

&lt;p&gt;Suppose you want to add automatic article-tag suggestions to a CMS.&lt;/p&gt;

&lt;p&gt;The weak workflow is predictable. You ask for a summary of classification models, embedding-based tagging, and prompt-based extraction. You get a clean answer, feel informed, then start coding. Two days later you discover inconsistent tags, slow moderation screens, duplicated retries, and no way to audit why a tag was assigned.&lt;/p&gt;

&lt;p&gt;The stronger workflow starts from the shipping constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong question
&lt;/h3&gt;

&lt;p&gt;"What are the best ways to generate tags from article text?"&lt;/p&gt;

&lt;p&gt;That produces a taxonomy lesson.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better question
&lt;/h3&gt;

&lt;p&gt;"We need draft-time tag suggestions inside an editorial CMS. Editors must be able to override suggestions. Suggestions should not block publishing. We need deterministic enough behavior to avoid tag drift over time. What architecture gives us acceptable latency, auditability, and maintainability?"&lt;/p&gt;

&lt;p&gt;Now the model can actually help. It may push you toward a queued enrichment job, a versioned suggestion policy, and a persisted explanation field so editors can see why a suggestion appeared.&lt;/p&gt;

&lt;p&gt;A useful implementation sketch might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateTagSuggestions&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;Article&lt;/span&gt; &lt;span class="nv"&gt;$article&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$article&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isDraft&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="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$result&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="n"&gt;taggingService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;suggest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;articleId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$article&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="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$article&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$article&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;SuggestedTagSet&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;updateOrCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'article_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$article&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;'policy_version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;policyVersion&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'tags'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'rationale'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'generated_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="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what matters here. The interesting part is not "call model, get tags." The interesting part is &lt;strong&gt;policy versioning, non-blocking execution, and persistence of rationale&lt;/strong&gt;. That is shipping context.&lt;/p&gt;

&lt;p&gt;From there, your notes should immediately capture questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should editors see stale suggestions or none at all during failures?&lt;/li&gt;
&lt;li&gt;Do we overwrite prior suggestions or store history?&lt;/li&gt;
&lt;li&gt;What causes a re-generation?&lt;/li&gt;
&lt;li&gt;How do we prevent model drift from polluting taxonomy quality?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a much better learning outcome than a polished explanation of prompt engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn Learning Into Tests Before You Trust It
&lt;/h2&gt;

&lt;p&gt;A useful rule: if the topic can affect production behavior, your learning workflow should end with tests or at least a test plan.&lt;/p&gt;

&lt;p&gt;This is where AI can be genuinely effective. Once you have a candidate design, ask the model to generate the test matrix from the failure modes you already identified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example test prompts
&lt;/h3&gt;

&lt;p&gt;Ask for tests like an engineer, not like a student:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Given this service and job flow, generate the test cases that protect against:
- duplicate retries
- partial external API failures
- stale cached outputs
- invalid editor overrides
- regression when taxonomy rules change

Return them grouped into unit, integration, and queue/retry scenarios.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That often gets you to a stronger suite faster than starting from a blank file.&lt;/p&gt;

&lt;p&gt;A Laravel-style test outline might end up looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'does not create duplicate suggestion records on retry'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Article&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;draft&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="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GenerateTagSuggestions&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GenerateTagSuggestions&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$article&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="nc"&gt;SuggestedTagSet&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'keeps publishing path independent from tag suggestion failure'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="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;TaggingService&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;'suggest'&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;andThrow&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;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rate limited'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;app&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;instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TaggingService&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="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Article&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;draft&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;PublishArticle&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$article&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not that the model writes perfect tests. It usually does not. The point is that it can help you enumerate failure surfaces quickly, which is exactly what good learning should produce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where LLM Learning Workflows Fail
&lt;/h2&gt;

&lt;p&gt;The biggest failure mode is false closure. The model sounds organized, so the engineer stops digging.&lt;/p&gt;

&lt;p&gt;You should assume these weaknesses unless proven otherwise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It hides uncertainty behind fluent prose.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It smooths over important edge cases.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It underestimates operational burden.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It gives symmetrical tradeoffs where a stronger recommendation is warranted.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means your workflow needs explicit guardrails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three guardrails that matter
&lt;/h3&gt;

&lt;p&gt;First, separate &lt;strong&gt;source capture&lt;/strong&gt; from &lt;strong&gt;decision output&lt;/strong&gt;. Keep the raw notes distinct from the final recommendation. Otherwise you lose track of what came from docs, what came from inference, and what came from your own architecture constraints.&lt;/p&gt;

&lt;p&gt;Second, force the model to state what it does &lt;strong&gt;not&lt;/strong&gt; know from the provided context. Missing constraints are often more important than the explanation itself.&lt;/p&gt;

&lt;p&gt;Third, require a recommendation with a rejection reason. If the output ends with "it depends," you probably asked the wrong question or gave no real constraints.&lt;/p&gt;

&lt;p&gt;A strong final prompt often looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Based on everything above, make a recommendation for this codebase.
Choose one default approach.
Then list:
- what we are intentionally not optimizing for
- what could make this decision wrong in 6 months
- what we should measure after rollout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That forces a more senior shape of answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Rule: Learn Toward the Next Commit
&lt;/h2&gt;

&lt;p&gt;The best AI learning workflow for engineers is not a reading workflow. It is a &lt;strong&gt;commit-preparation workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Use the model to get from vague topic knowledge to the next set of concrete moves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what to change&lt;/li&gt;
&lt;li&gt;what to avoid&lt;/li&gt;
&lt;li&gt;what to test&lt;/li&gt;
&lt;li&gt;what to monitor&lt;/li&gt;
&lt;li&gt;what decision to document&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your notes do not help you open a PR, write an ADR, or tighten a test suite, they are probably still too academic.&lt;/p&gt;

&lt;p&gt;That is the real standard. Not "did the model explain it well?" but &lt;strong&gt;did the workflow reduce uncertainty at the code and system level?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For engineers shipping real systems, that is the only kind of learning that compounds. Use AI to compress research, but force it to end in architecture pressure, implementation artifacts, and testable decisions. Anything less is just smarter procrastination.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/ai-learning-workflows-engineers-shipping-context/" rel="noopener noreferrer"&gt;https://qcode.in/ai-learning-workflows-engineers-shipping-context/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>softwareengineering</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Using Test Impact Analysis on Slow Laravel Test Suites Without Losing Trust</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Wed, 19 Aug 2026 05:19:05 +0000</pubDate>
      <link>https://dev.to/saqueib/using-test-impact-analysis-on-slow-laravel-test-suites-without-losing-trust-4efa</link>
      <guid>https://dev.to/saqueib/using-test-impact-analysis-on-slow-laravel-test-suites-without-losing-trust-4efa</guid>
      <description>&lt;p&gt;If your Laravel suite is slow enough that developers hesitate before running it, you do not really have a fast feedback loop. You have a compliance ritual. That becomes a bigger problem once agents enter the workflow, because agents amplify whatever loop you give them. If verification takes twelve minutes, the agent either waits uselessly, skips checks, or pushes too much uncertainty downstream.&lt;/p&gt;

&lt;p&gt;The practical answer for most PHP teams is &lt;strong&gt;test impact analysis locally, full-suite verification in CI, and explicit rules for when selective execution is too risky&lt;/strong&gt;. That gives you a fast inner loop without lying to yourself about what counts as real confidence.&lt;/p&gt;

&lt;p&gt;For Laravel teams, this is now a serious option. Pest 5 introduced built-in test impact analysis through &lt;code&gt;--tia&lt;/code&gt;, and Laravel’s testing stack already gives you the parallel execution hooks you need to keep fallback runs sane. The hard part is not turning the feature on. The hard part is designing a workflow that stays trustworthy when the codebase, the team, and the suite get messy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem Is Not Just Runtime
&lt;/h2&gt;

&lt;p&gt;A slow suite hurts in more than one way.&lt;/p&gt;

&lt;p&gt;The obvious cost is wall-clock time. A developer changes one policy, one listener, or one validation rule, then waits minutes for a result. That is annoying, but it is still the shallow version of the problem.&lt;/p&gt;

&lt;p&gt;The deeper cost is behavioral. Once the suite becomes expensive, people stop using it as a steering mechanism. They batch unrelated edits together. They verify less often. They defer broad checks until the end of the branch. Agents do the same thing even faster. Instead of validating each change in small increments, they accumulate risk and hope the next big run explains what broke.&lt;/p&gt;

&lt;p&gt;That is why &lt;strong&gt;Laravel test impact analysis&lt;/strong&gt; matters. It is not only about making tests faster. It is about restoring a development rhythm where verification can happen after small, frequent changes.&lt;/p&gt;

&lt;p&gt;Pest’s TIA model is appealing for exactly this reason. The first run builds a dependency graph from coverage data, using a driver such as PCOV or Xdebug. Later runs look at what changed, rerun the tests that depend on those files, and replay cached results for the rest. Pest’s own release docs position this as a local development feature, not as a substitute for full CI verification, and that framing is correct.&lt;/p&gt;

&lt;p&gt;If you ignore that framing, you create a false sense of safety. If you respect it, you get a much better developer loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a healthy loop should feel like
&lt;/h3&gt;

&lt;p&gt;A healthy Laravel testing workflow should make these things cheap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rerunning a narrow slice after a small refactor&lt;/li&gt;
&lt;li&gt;verifying a bug fix before moving to the next file&lt;/li&gt;
&lt;li&gt;letting an agent validate each patch instead of dumping one giant speculative change&lt;/li&gt;
&lt;li&gt;widening the scope when the blast radius starts to grow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters most. Good teams do not ask one testing mode to solve everything. They use different levels of verification for different kinds of uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Test Impact Analysis Actually Wins
&lt;/h2&gt;

&lt;p&gt;The biggest win is not the first-run benchmark. It is the reduction in friction after the first run.&lt;/p&gt;

&lt;p&gt;Once the dependency graph exists, your team can stop treating the whole suite as the default response to every tiny edit. A small change to a controller, request object, or service class should not force the same verification cost as a migration rewrite or authentication refactor.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but many Laravel codebases still behave as if every touched file deserves a full ceremonial run. That habit survives because tooling used to make the alternative awkward. It is less defensible now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best-fit projects
&lt;/h3&gt;

&lt;p&gt;Test impact analysis works especially well in Laravel projects with these properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a real separation between unit and feature tests&lt;/li&gt;
&lt;li&gt;request flows that are tested by intent, not by giant catch-all files&lt;/li&gt;
&lt;li&gt;factories and fixtures with predictable side effects&lt;/li&gt;
&lt;li&gt;limited hidden state in shared helpers or global bootstrapping&lt;/li&gt;
&lt;li&gt;developers who work in small batches instead of all-day mega-branches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those codebases, TIA becomes a force multiplier. It keeps local checks cheap while preserving enough relevance that the result is useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters for agent-assisted development
&lt;/h3&gt;

&lt;p&gt;Agents are not patient engineers. They are throughput machines. If you give them a ten-minute verification cycle, they will either use it badly or avoid it. If you give them a three-second impacted run plus a clear escalation rule for wider checks, they become much more useful.&lt;/p&gt;

&lt;p&gt;That is the real opportunity here. TIA turns verification from an end-of-branch event into an every-few-minutes event. That is exactly the cadence agents need.&lt;/p&gt;

&lt;h3&gt;
  
  
  A realistic local command set
&lt;/h3&gt;

&lt;p&gt;Do not overcomplicate the first rollout. Three commands are enough for most teams:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./vendor/bin/pest --parallel --tia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:quick"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"php artisan test --parallel --stop-on-failure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:full"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"php artisan test --parallel --recreate-databases"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally boring.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;test:impact&lt;/code&gt; is the default inner loop. &lt;code&gt;test:quick&lt;/code&gt; is the broader local check before a push when the change touches meaningful app behavior. &lt;code&gt;test:full&lt;/code&gt; is the expensive reset when the branch has become wide enough that you want a fresh, broad pass with recreated databases.&lt;/p&gt;

&lt;p&gt;You can add more scripts later, but the bigger mistake is starting with a clever matrix of commands nobody remembers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trust Boundary Has To Be Explicit
&lt;/h2&gt;

&lt;p&gt;The most common failure with selective testing is not a bug in the tool. It is a bug in the team’s interpretation of green.&lt;/p&gt;

&lt;p&gt;If you do not define this clearly, people start treating a fast impacted run as equivalent to a branch-wide verification result. It is not.&lt;/p&gt;

&lt;p&gt;The safest rule is brutally simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;impacted green&lt;/strong&gt; means the recent change is probably safe to continue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;broader local green&lt;/strong&gt; means the branch is likely stable enough to push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;full-suite CI green&lt;/strong&gt; means the branch is ready to trust&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those three signals are related, but they are not interchangeable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why CI should stay conservative
&lt;/h3&gt;

&lt;p&gt;Pest’s official guidance says TIA is for local development and that CI should run the full suite against a clean checkout. That is the right architectural line.&lt;/p&gt;

&lt;p&gt;There are good reasons for that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI should not depend on a developer’s local cache state&lt;/li&gt;
&lt;li&gt;protected-branch verification should be deterministic and boring&lt;/li&gt;
&lt;li&gt;cross-cutting failures often emerge only in a clean environment&lt;/li&gt;
&lt;li&gt;replayed results are valuable for iteration, but full-system trust belongs to a fresh run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laravel’s parallel testing support is strong enough that full-suite CI does not have to be painfully slow if the suite is structured competently. Use &lt;code&gt;php artisan test --parallel&lt;/code&gt;, tune process count, and shard later only if you actually need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A sane CI shape
&lt;/h3&gt;

&lt;p&gt;A straightforward GitHub Actions job is enough to enforce the trust boundary:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;suite&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;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;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.4'&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;pcov&lt;/span&gt;
      &lt;span class="pi"&gt;-&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 --prefer-dist&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;php artisan test --parallel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not glamorous, but that is the point. &lt;strong&gt;Local verification can be clever. CI should be dependable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to go further, a useful pattern is to refresh the TIA baseline on merges to &lt;code&gt;main&lt;/code&gt; so developers pull fresh mapping data locally. That supports fast replay behavior without turning CI itself into a selective-execution system of record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The High-Risk Changes Where Impacted-Only Is Not Enough
&lt;/h2&gt;

&lt;p&gt;This is where teams need judgment instead of slogans.&lt;/p&gt;

&lt;p&gt;Selective execution is strongest when the change is narrow and the dependency graph tells a clear story. It becomes weaker when the edit changes shared behavior, bootstrapping, or infrastructure that many tests rely on indirectly.&lt;/p&gt;

&lt;p&gt;I would treat these as &lt;strong&gt;default escalation zones&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;service providers and container bindings&lt;/li&gt;
&lt;li&gt;authentication, authorization, policies, and middleware wiring&lt;/li&gt;
&lt;li&gt;framework and package upgrades&lt;/li&gt;
&lt;li&gt;global config changes and env-dependent behavior&lt;/li&gt;
&lt;li&gt;migrations that alter heavily used tables or indexes&lt;/li&gt;
&lt;li&gt;changes to base test classes, custom assertions, traits, or shared helpers&lt;/li&gt;
&lt;li&gt;factories, seeders, and fixtures with broad reuse&lt;/li&gt;
&lt;li&gt;queue, cache, event, broadcast, and notification infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not cases where TIA is useless. They are cases where &lt;strong&gt;TIA should be the first signal, not the last one&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migration changes are especially deceptive
&lt;/h3&gt;

&lt;p&gt;A schema change often looks narrower than it really is.&lt;/p&gt;

&lt;p&gt;Suppose you rename a column on a commonly queried table, tweak a foreign key, or change default values that influence model state. The immediate impacted set may be small enough to look comforting. The real application blast radius may not be.&lt;/p&gt;

&lt;p&gt;For that class of change, I would use a two-step rule:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;run impacted tests immediately for fast feedback&lt;/li&gt;
&lt;li&gt;widen to a broader parallel run before you trust the branch&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That keeps the inner loop fast without mistaking local confidence for system-wide certainty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared test infrastructure is another danger zone
&lt;/h3&gt;

&lt;p&gt;If you change a custom test trait, base &lt;code&gt;TestCase&lt;/code&gt;, helper that signs users in, or a fixture factory used across dozens of flows, the suite can fail in surprising places. Some of those failures may be obvious dependencies. Others are just side effects hiding behind convenience code.&lt;/p&gt;

&lt;p&gt;That is also why test architecture quality still matters. TIA can reduce the runtime cost of a messy suite, but it cannot magically make a highly coupled suite easy to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Testing Is What Makes the Safety Net Practical
&lt;/h2&gt;

&lt;p&gt;Impact analysis gets most of the attention because it is the shiny feature. For Laravel teams, parallel testing is what keeps the whole strategy honest.&lt;/p&gt;

&lt;p&gt;If your fallback full run is still unbearable, people will avoid it. Then the workflow collapses into impacted-only by habit, even if nobody says that out loud.&lt;/p&gt;

&lt;p&gt;Laravel’s official testing docs already give you the baseline: &lt;code&gt;php artisan test --parallel&lt;/code&gt;, automatic per-process test databases, and hooks for process and database setup. Use them properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource isolation is where many teams fail
&lt;/h3&gt;

&lt;p&gt;A lot of "parallel is flaky" complaints are not actually about parallelism. They are about shared resources that were never made safe for concurrency.&lt;/p&gt;

&lt;p&gt;Typical offenders include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cache prefixes shared across processes&lt;/li&gt;
&lt;li&gt;temp files written to one global path&lt;/li&gt;
&lt;li&gt;seeded data that assumes singleton state&lt;/li&gt;
&lt;li&gt;external service doubles that reuse global ports or files&lt;/li&gt;
&lt;li&gt;SQLite file contention when tests write concurrently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laravel gives you the hooks to isolate this cleanly:&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;Illuminate\Support\Facades\Artisan&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\ParallelTesting&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\TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;ParallelTesting&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;setUpProcess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&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;'cache.prefix'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tests_'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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;'filesystems.disks.local.root'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'framework/testing/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;ParallelTesting&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;setUpTestDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$database&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;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Artisan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'db:seed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'--class'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'RequiredTestSeeder'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;ParallelTesting&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;setUpTestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;TestCase&lt;/span&gt; &lt;span class="nv"&gt;$testCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Per-test-case setup when specific shared resources need token isolation.&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the sort of implementation detail teams skip when they want the speedup without the engineering work. Then they conclude that the tool is unreliable. Usually the problem is that the suite was relying on hidden global state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Profile before you over-engineer
&lt;/h3&gt;

&lt;p&gt;Before inventing new layers, run Pest with &lt;code&gt;--profile&lt;/code&gt; and look at the slowest tests. In many Laravel suites, a small number of pathological tests dominate total runtime.&lt;/p&gt;

&lt;p&gt;Typical causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repeated full-database seeding inside unrelated tests&lt;/li&gt;
&lt;li&gt;expensive external process setup&lt;/li&gt;
&lt;li&gt;feature tests that cover too much workflow in one file&lt;/li&gt;
&lt;li&gt;chatty factories creating large object graphs for trivial assertions&lt;/li&gt;
&lt;li&gt;browser or integration tests mixed into the same default loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Impact analysis helps with selection. It does not excuse waste inside the selected tests. Fixing the slowest ten often matters more than adding another clever test command.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Production-Ready Workflow Looks Like
&lt;/h2&gt;

&lt;p&gt;The best rollout is not "turn on TIA and trust the magic." It is a layered workflow with escalation points.&lt;/p&gt;

&lt;p&gt;Here is the version I would recommend to most PHP teams.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Default to impacted runs during active development
&lt;/h3&gt;

&lt;p&gt;When a developer or agent is changing one service, one request class, one controller, or one policy, &lt;code&gt;test:impact&lt;/code&gt; should be the default response. It keeps the inner loop tight and makes frequent verification realistic.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Widen the scope when the branch stops being narrow
&lt;/h3&gt;

&lt;p&gt;Once the work touches shared infrastructure, data shape, or multiple bounded areas, run &lt;code&gt;test:quick&lt;/code&gt;. That broader parallel pass catches issues the narrow loop was never meant to certify.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Keep full-suite CI as the merge truth
&lt;/h3&gt;

&lt;p&gt;Protected branches should still rely on a full run from a clean checkout. If you blur that line, you lose the only signal everyone can trust equally.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Write the escalation rules down
&lt;/h3&gt;

&lt;p&gt;Do not leave this to intuition alone. Put it in &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; or the team handbook.&lt;/p&gt;

&lt;p&gt;Something this short is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Use &lt;span class="sb"&gt;`test:impact`&lt;/span&gt; for local iteration on narrow changes.
Run &lt;span class="sb"&gt;`test:quick`&lt;/span&gt; before pushing changes that touch shared app behavior.
Run &lt;span class="sb"&gt;`test:full`&lt;/span&gt; locally after risky refactors, migrations, or shared test infrastructure changes.
CI remains the source of truth and always runs the full suite.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That documentation matters because it preserves meaning. Fast green stays useful without becoming sloppy green.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Split categories if your suite is structurally mixed
&lt;/h3&gt;

&lt;p&gt;If unit, feature, browser, and contract tests all live in one verification habit, selective testing still helps, but your team will keep mixing very different confidence levels together.&lt;/p&gt;

&lt;p&gt;A better model is often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unit and feature tests in the fast daily loop&lt;/li&gt;
&lt;li&gt;browser or end-to-end tests as a separate, less frequent layer&lt;/li&gt;
&lt;li&gt;heavy cross-service contract checks reserved for CI or targeted pre-merge runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not to produce a pretty pyramid diagram. The point is to stop asking one command to carry every kind of certainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Rule That Actually Works
&lt;/h2&gt;

&lt;p&gt;If your Laravel suite is too slow, do not pick between "run everything" and "run less." That framing is weak.&lt;/p&gt;

&lt;p&gt;The stronger approach is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;strong&gt;impact analysis&lt;/strong&gt; to make local iteration fast enough to be habitual&lt;/li&gt;
&lt;li&gt;use &lt;strong&gt;parallel broader runs&lt;/strong&gt; to absorb uncertainty when the blast radius grows&lt;/li&gt;
&lt;li&gt;use &lt;strong&gt;full-suite CI&lt;/strong&gt; to protect the branch with a deterministic signal&lt;/li&gt;
&lt;li&gt;treat &lt;strong&gt;cross-cutting changes&lt;/strong&gt; as escalation cases, not normal cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the decision rule I would give any PHP team building with agents, frequent refactors, or simply a codebase large enough that the old full-suite-only habit has become a drag.&lt;/p&gt;

&lt;p&gt;Pest 5’s &lt;a href="https://pestphp.com/docs/pest5-now-available" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; are worth reading for the TIA model, and Laravel’s &lt;a href="https://laravel.com/docs/13.x/testing#running-tests-in-parallel" rel="noopener noreferrer"&gt;testing documentation&lt;/a&gt; remains the practical reference for parallel setup and database behavior. Pest’s &lt;a href="https://pestphp.com/docs/cli-api-reference" rel="noopener noreferrer"&gt;CLI reference&lt;/a&gt; is also useful once you start tuning the workflow.&lt;/p&gt;

&lt;p&gt;The memorable version is short: &lt;strong&gt;selective locally, exhaustive in CI, and skeptical whenever the change smells global&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is how you get faster feedback without lowering the bar.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/laravel-test-impact-analysis-for-slow-suites/" rel="noopener noreferrer"&gt;https://qcode.in/laravel-test-impact-analysis-for-slow-suites/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>testing</category>
      <category>ci</category>
    </item>
    <item>
      <title>Pest 5 Makes Agent Verification Feel Like a Real Testing Workflow</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Thu, 13 Aug 2026 05:23:40 +0000</pubDate>
      <link>https://dev.to/saqueib/pest-5-makes-agent-verification-feel-like-a-real-testing-workflow-830</link>
      <guid>https://dev.to/saqueib/pest-5-makes-agent-verification-feel-like-a-real-testing-workflow-830</guid>
      <description>&lt;p&gt;If your team is experimenting with coding agents, Pest 5 changes the conversation in a useful way. It stops agent safety from being a vague review culture problem and turns it into a verification design problem. That is the right frame. Agents will keep producing plausible code quickly. The only durable answer is to make correctness cheap to prove.&lt;/p&gt;

&lt;p&gt;For Laravel and PHP teams, the practical stack is now much clearer: &lt;strong&gt;use Pest tests for hard business truth, static analysis for structural correctness, evals for fuzzy behavior, and human review for judgment calls&lt;/strong&gt;. Pest 5 matters because it gives these pieces a workflow shape that actually fits agent-assisted development instead of treating AI as a separate novelty lane.&lt;/p&gt;

&lt;p&gt;The mistake to avoid is simple: do not bolt an agent onto a weak codebase and expect code review to absorb the risk. If the rules that matter are not encoded already, the agent did not create your problem. It exposed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shift: Verification First, Agent Second
&lt;/h2&gt;

&lt;p&gt;Most teams still evaluate agent output backward. They start with the generated diff, then ask a reviewer to mentally simulate whether it is safe. That does not scale. It did not scale before AI either, but agents make the failure obvious because they can produce ten reviewable changes in the time a human properly reasons through one.&lt;/p&gt;

&lt;p&gt;Pest 5 pushes a better operating model. Its current release and docs position the framework around faster feedback loops, test impact analysis, an agent verification command, browser testing, and eval-oriented workflows for AI-heavy applications. The important part is not the feature checklist. The important part is what that checklist implies: &lt;strong&gt;verification should be the product boundary for agent-written code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means you need a clear split of responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit and feature tests&lt;/strong&gt; own business rules and regression checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static analysis&lt;/strong&gt; owns type discipline, framework misuse, and structural drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evals&lt;/strong&gt; own broader output quality where exact equality is the wrong assertion model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human review&lt;/strong&gt; owns architecture, scope discipline, naming, and product intent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you keep those boundaries fuzzy, agents become a source of anxiety. If you make them explicit, agents become another producer feeding a strong acceptance pipeline.&lt;/p&gt;

&lt;p&gt;This is why I think the real value of Pest 5 is not "AI support." It is that Pest is finally treating verification as something that can be shaped around agent workflows without turning the test suite into theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Put Non-Negotiable Rules in Tests
&lt;/h2&gt;

&lt;p&gt;If a broken rule would create money loss, permission leaks, bad state transitions, or user-visible corruption, it belongs in a normal test. Not in a prompt. Not in a reviewer checklist. Not in tribal knowledge.&lt;/p&gt;

&lt;p&gt;This is the first thing to fix in a Laravel codebase before you let agents touch meaningful flows. The suite has to answer, in executable form, what must never change.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should live in Pest tests
&lt;/h3&gt;

&lt;p&gt;In practice, these categories almost always belong there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pricing and tax calculations&lt;/li&gt;
&lt;li&gt;Authorization and policy outcomes&lt;/li&gt;
&lt;li&gt;State machine transitions&lt;/li&gt;
&lt;li&gt;Validation invariants&lt;/li&gt;
&lt;li&gt;Idempotency behavior for jobs, webhooks, and retries&lt;/li&gt;
&lt;li&gt;Data transformation rules used by downstream systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not "nice to have" tests. Those are the contract that makes agent iteration safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: pricing logic the agent cannot negotiate with
&lt;/h3&gt;

&lt;p&gt;Suppose an agent refactors a checkout service to reduce duplication. The code might look cleaner and still be wrong in exactly the place that matters. This is where a narrow, explicit Pest suite earns its keep.&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;App\Domain\Billing\Cart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Domain\Billing\Money&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Domain\Billing\PercentageDiscount&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="s1"&gt;'never drops below zero when discounts exceed subtotal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$cart&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;Cart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nv"&gt;$total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$cart&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;applyDiscount&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;PercentageDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&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;total&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="nv"&gt;$total&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toInt&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;toBe&lt;/span&gt;&lt;span class="p"&gt;(&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applies store credit after discount rules without producing negative totals'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$cart&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;Cart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nv"&gt;$total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$cart&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;applyDiscount&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;PercentageDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&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;applyStoreCredit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9000&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;total&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="nv"&gt;$total&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toInt&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;toBe&lt;/span&gt;&lt;span class="p"&gt;(&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'keeps cents precise across discount math'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$cart&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;Cart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1999&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nv"&gt;$total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$cart&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;applyDiscount&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;PercentageDiscount&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;total&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="nv"&gt;$total&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toInt&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;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1799&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;These tests are not interesting in a literary sense. Good. They should be boring and merciless. An agent can refactor the service, swap collaborators, rename classes, or optimize internals. What it cannot do is silently break the money rules without a visible failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the suite sharp, not bloated
&lt;/h3&gt;

&lt;p&gt;A common overreaction is to compensate for AI with giant end-to-end coverage everywhere. That usually makes the feedback loop worse.&lt;/p&gt;

&lt;p&gt;What you want is a layered suite with different response times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small unit tests for hard domain rules&lt;/li&gt;
&lt;li&gt;Focused feature tests for Laravel request and policy flows&lt;/li&gt;
&lt;li&gt;A limited number of integration tests for database, queues, mail, or external boundaries&lt;/li&gt;
&lt;li&gt;Browser tests only where UI behavior really matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If every rule is validated only through a slow browser or full-stack flow, test impact analysis becomes less useful and agents lose the speed advantage you wanted in the first place.&lt;/p&gt;

&lt;p&gt;The test suite should be opinionated about cost. Cheap tests should guard expensive mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Use Static Analysis to Catch Structural Drift Early
&lt;/h2&gt;

&lt;p&gt;Tests catch behavior. They do not reliably catch a service returning the wrong shape, a nullable leak creeping through a boundary, a collection changing element type, or a helper method becoming a soft-typed dumping ground. Agents are especially good at creating those kinds of problems because the code still looks plausible.&lt;/p&gt;

&lt;p&gt;That is why static analysis needs to be part of agent verification, not a separate quality initiative you keep postponing.&lt;/p&gt;

&lt;p&gt;Pest 5’s broader ecosystem message matters here too. The official release material ties Pest to a stronger toolchain around verification, not just test syntax. That matches reality: Laravel teams need &lt;strong&gt;Pest plus PHPStan plus Larastan&lt;/strong&gt;, not Pest alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make contracts explicit where agents touch them
&lt;/h3&gt;

&lt;p&gt;Application services, data mappers, and domain actions benefit a lot from explicit shapes. Once you document the return contract tightly, both humans and tools get less room to lie to themselves.&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;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Actions\Orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BuildOrderSummary&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * @return array{
     *     id: int,
     *     customer_email: string,
     *     currency: string,
     *     lines: list&amp;lt;array{
     *         sku: string,
     *         name: string,
     *         quantity: int,
     *         total_cents: int
     *     }&amp;gt;,
     *     total_cents: int
     * }
     */&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;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;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;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'customer_email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'currency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'lines'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s1"&gt;'sku'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sku&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="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'quantity'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'total_cents'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total_cents&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;values&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;all&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'total_cents'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total_cents&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;That docblock is not decorative. It gives PHPStan and Larastan something strong enough to reason about when an agent starts "cleaning up" a serializer or changing a downstream consumer.&lt;/p&gt;

&lt;p&gt;Without those contracts, the typical failure mode looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent swaps an integer for a float because it feels convenient.&lt;/li&gt;
&lt;li&gt;A collection becomes lazy where an eager array was assumed.&lt;/li&gt;
&lt;li&gt;A nullable property leaks into mail or queue payload logic.&lt;/li&gt;
&lt;li&gt;The code passes casual review because the diff feels tidy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Static analysis is the right place to kill those changes fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  A sane Laravel baseline for agent work
&lt;/h3&gt;

&lt;p&gt;If you want a practical bar, this is a good starting point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;strong&gt;PHPStan&lt;/strong&gt; and &lt;strong&gt;Larastan&lt;/strong&gt; in CI on every agent-authored change.&lt;/li&gt;
&lt;li&gt;Treat new ignores as debt that needs explanation, not as routine cleanup.&lt;/li&gt;
&lt;li&gt;Add return types, generic collections, and array shapes to application services first.&lt;/li&gt;
&lt;li&gt;Tighten hot paths before edge paths. Orders, billing, auth, webhooks, and exports usually deserve the earliest attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not to reach some abstract purity level. The point is to shrink the space where an agent can introduce quiet structural decay and still look productive.&lt;/p&gt;

&lt;p&gt;Official references are worth linking where readers want to go deeper: &lt;a href="https://pestphp.com/docs/pest5-now-available" rel="noopener noreferrer"&gt;Pest 5 announcement&lt;/a&gt;, &lt;a href="https://pestphp.com/docs/agent" rel="noopener noreferrer"&gt;Agent plugin docs&lt;/a&gt;, &lt;a href="https://phpstan.org" rel="noopener noreferrer"&gt;PHPStan&lt;/a&gt;, and &lt;a href="https://github.com/larastan/larastan" rel="noopener noreferrer"&gt;Larastan&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Use Agent Verification for Fast Probes, Not Permanent Coverage
&lt;/h2&gt;

&lt;p&gt;The Pest agent plugin is one of the more practical pieces in this release because it acknowledges a real workflow need: sometimes the agent does not need a new permanent test yet. It needs a one-off proof that the change works inside the real test environment.&lt;/p&gt;

&lt;p&gt;That is a very different thing from saying "the agent should just click around and hope." The plugin’s current docs describe a single verification command that can exercise backend behavior directly and, with browser tooling installed, drive real UI flows as well. That is useful precisely because it stays inside the verification boundary instead of inventing a separate AI sandbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  Good uses for one-off agent verification
&lt;/h3&gt;

&lt;p&gt;This is where I think it fits best:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Route still returns &lt;code&gt;200&lt;/code&gt; after a controller refactor&lt;/li&gt;
&lt;li&gt;Policy still blocks a user class correctly&lt;/li&gt;
&lt;li&gt;Dashboard still renders after a Livewire or Blade change&lt;/li&gt;
&lt;li&gt;Queue side effect still happens after an event wiring change&lt;/li&gt;
&lt;li&gt;Form flow still works before you decide whether it deserves a permanent browser test&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bad uses for one-off agent verification
&lt;/h3&gt;

&lt;p&gt;This is where teams will misuse it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replacing durable tests with ad hoc probes&lt;/li&gt;
&lt;li&gt;Treating a successful one-off check as proof of broad correctness&lt;/li&gt;
&lt;li&gt;Letting the agent invent vague success criteria on the fly&lt;/li&gt;
&lt;li&gt;Skipping normal feature tests because the probe was "good enough"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule should be strict: &lt;strong&gt;agent verification probes are temporary evidence, not long-term coverage&lt;/strong&gt;. If a behavior matters repeatedly, promote it into a real test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: one-off probe after a dashboard access change
&lt;/h3&gt;

&lt;p&gt;The official docs show the shape of this pattern with a &lt;code&gt;--agent&lt;/code&gt; command. In practice, a Laravel team might use it like this after an authorization or rendering change:&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/pest &lt;span class="nt"&gt;--agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'$user = \\App\\Models\\User::factory()-&amp;gt;create(); $this-&amp;gt;actingAs($user)-&amp;gt;get("/dashboard")-&amp;gt;assertOk();'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useful because it checks the actual application boundary quickly. But if the dashboard access rule is business-critical, stop there only once. The second time that flow matters, turn it into a committed feature test.&lt;/p&gt;

&lt;p&gt;This is the right mental model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Probe once to unblock local iteration&lt;/li&gt;
&lt;li&gt;Promote repeated or risky behavior into the suite&lt;/li&gt;
&lt;li&gt;Never confuse fast evidence with durable coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction will save your team from creating a pile of unverifiable agent folklore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Treat Evals as Behavioral Sweeps, Not Truth Machines
&lt;/h2&gt;

&lt;p&gt;Evals are the most misunderstood part of this stack because teams tend to swing between two bad extremes. Either they avoid them because they sound fuzzy, or they overuse them because they sound modern.&lt;/p&gt;

&lt;p&gt;The right use is narrower and more valuable: evals are great for &lt;strong&gt;broad behavioral assessment when exact string equality or a single assertion is the wrong testing model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That matters a lot for teams building AI features into Laravel apps, but it also matters for agent-driven code changes where you want to validate classes of behavior across many cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  What belongs in evals
&lt;/h3&gt;

&lt;p&gt;Good candidates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prompt-driven summarization quality&lt;/li&gt;
&lt;li&gt;Classification consistency across datasets&lt;/li&gt;
&lt;li&gt;Whether generated content follows policy constraints&lt;/li&gt;
&lt;li&gt;Whether a support assistant refuses unsafe requests correctly&lt;/li&gt;
&lt;li&gt;Whether a code transformation preserved behavior across a bank of fixtures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What does not belong there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refund arithmetic
n- Role-based authorization truth&lt;/li&gt;
&lt;li&gt;Validation rules with deterministic outcomes&lt;/li&gt;
&lt;li&gt;Anything you can express cleanly as a direct assertion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the outcome is exact and deterministic, use a normal test. Evals are not a fashionable replacement for good engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: evaluating a support assistant policy boundary
&lt;/h3&gt;

&lt;p&gt;A Laravel team shipping an internal or customer-facing assistant might use dataset-style cases to test refusal and escalation behavior. Pest’s eval tooling makes sense here because "correct" is often about policy adherence across varied prompts, not one exact sentence.&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'handles refund policy scenarios consistently'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$message&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;$expectedLabel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SupportAgent&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResponsePolicyClassifier&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$reply&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="nv"&gt;$label&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;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expectedLabel&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;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'I want a refund for an order placed 5 minutes ago'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'allow_refund_path'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Refund me for a non-refundable item from 8 months ago'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'deny_with_policy_reason'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Can you refund my friend\'s order if I know their email?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'deny_identity_mismatch'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'I was charged twice, what should I do?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'route_to_human_or_duplicate_charge_flow'&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;That example still looks test-like because it should. The important shift is conceptual: you are validating behavior quality across a realistic set of cases, not pretending one string comparison will capture the whole policy surface.&lt;/p&gt;

&lt;p&gt;For AI-heavy products, this is the missing layer between deterministic tests and manual spot checks. It gives teams a way to keep agent and model behavior under pressure without faking precision they do not actually have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Make Test Impact Analysis Serve the Loop, Not Replace the Gate
&lt;/h2&gt;

&lt;p&gt;Test impact analysis is the feature that will probably change team behavior fastest, because feedback speed is where most agent workflows fall apart. The official Pest 5 announcement positions the TIA engine as a way to rerun only affected tests after changes. If that works well in your codebase, it is a big operational win.&lt;/p&gt;

&lt;p&gt;But there is a trap here too. Teams will be tempted to treat changed-area test selection as if it were the whole safety model. That is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use test impact analysis to accelerate local verification and agent iteration. Do not use it as an excuse to weaken merge gates.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What the workflow should look like
&lt;/h3&gt;

&lt;p&gt;A strong Laravel pipeline for agent-authored work usually wants three speeds:&lt;/p&gt;

&lt;h3&gt;
  
  
  Fast local loop
&lt;/h3&gt;

&lt;p&gt;This is what the agent or developer runs repeatedly during implementation:&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/pest &lt;span class="nt"&gt;--dirty&lt;/span&gt;
vendor/bin/phpstan analyse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal here is cheap confidence. The changed-area rerun should answer, "did this edit obviously break the things it touches?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Risk-targeted checks
&lt;/h3&gt;

&lt;p&gt;If the change touches policies, serialization, or UI flows, run the extra layer that matches the risk:&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/pest tests/Feature/Auth
vendor/bin/pest &lt;span class="nt"&gt;--agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'$user = \\App\\Models\\User::factory()-&amp;gt;create(); $this-&amp;gt;actingAs($user)-&amp;gt;get("/settings")-&amp;gt;assertOk();'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you are validating both the durable contract and the one-off path that mattered during the edit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full merge gate
&lt;/h3&gt;

&lt;p&gt;Before merge, the bar still needs to be broad:&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/pest
vendor/bin/phpstan analyse
php artisan &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--testsuite&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the codebase, you may also add browser tests, architectural tests, or a targeted eval suite. The point is that the full gate still exists. TIA narrows the inner loop. It does not get to redefine what production confidence means.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where TIA can mislead you
&lt;/h3&gt;

&lt;p&gt;There are a few failure modes worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Coupling is hidden, so the impacted set is smaller than the real risk surface.&lt;/li&gt;
&lt;li&gt;The suite is too integration-heavy, so changed-area speedups are weak.&lt;/li&gt;
&lt;li&gt;Critical business rules are missing, so fast feedback still misses expensive bugs.&lt;/li&gt;
&lt;li&gt;Teams start trusting fast green runs more than the real suite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the dangerous cultural bug. Fast feedback is for steering. Full verification is for merging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human Review Still Matters, but It Should Move Up the Stack
&lt;/h2&gt;

&lt;p&gt;The better your automated verification gets, the more valuable human review becomes because it stops being wasted on machine-checkable details.&lt;/p&gt;

&lt;p&gt;A good reviewer should not spend half their time re-deriving whether a discount can go negative or whether a nullable property escaped into a DTO. If those questions still dominate review, the pipeline is underbuilt.&lt;/p&gt;

&lt;p&gt;What humans should own instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the agent solving the right problem or just the nearest one?&lt;/li&gt;
&lt;li&gt;Did the abstraction improve clarity or create another layer for no reason?&lt;/li&gt;
&lt;li&gt;Are naming and boundaries getting stronger or weaker?&lt;/li&gt;
&lt;li&gt;Did the change increase hidden coupling?&lt;/li&gt;
&lt;li&gt;Is there an architectural consequence the local diff hides?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the level where senior engineering judgment matters. Everything below that should be pulled downward into tests, analysis, or evals over time.&lt;/p&gt;

&lt;p&gt;A useful rule for teams adopting agents is this: &lt;strong&gt;if reviewers catch the same class of mistake twice, automate it by the third time&lt;/strong&gt;. That one rule alone will make your agent workflow materially safer within a month.&lt;/p&gt;

&lt;p&gt;Pest 5 does not solve verification for you. It gives PHP teams a better place to anchor it. That is enough to matter.&lt;/p&gt;

&lt;p&gt;If you are running Laravel with coding agents today, the recommendation is straightforward. Start small, but make the stack explicit: durable Pest tests for core rules, PHPStan and Larastan for structure, one-off agent verification probes for fast iteration, evals for behavior classes that resist exact assertions, and a full merge gate that does not flinch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision rule:&lt;/strong&gt; if your confidence in an agent change still depends mostly on a reviewer reading the diff carefully, your verification system is too weak. Fix that before you scale the agent.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/pest-5-agent-verification-testing-problem/" rel="noopener noreferrer"&gt;https://qcode.in/pest-5-agent-verification-testing-problem/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI review can help with Laravel upgrades, but it should not make the decisions</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Wed, 12 Aug 2026 04:53:13 +0000</pubDate>
      <link>https://dev.to/saqueib/ai-review-can-help-with-laravel-upgrades-but-it-should-not-make-the-decisions-4lcf</link>
      <guid>https://dev.to/saqueib/ai-review-can-help-with-laravel-upgrades-but-it-should-not-make-the-decisions-4lcf</guid>
      <description>&lt;p&gt;Laravel upgrades are one of the easiest places to overestimate AI. It looks perfect for the job: large diffs, framework changes, repetitive refactors, and lots of surface area to scan. In practice, AI review is useful, but it is &lt;strong&gt;not&lt;/strong&gt; where the important upgrade decisions get made.&lt;/p&gt;

&lt;p&gt;I still use AI during Laravel upgrades, especially as a second pass. It catches renamed methods, outdated config shapes, stale imports, and framework-level inconsistencies faster than most humans want to admit. But the more expensive bugs in a real upgrade are usually not syntax bugs. They are judgment bugs. They come from misunderstanding how &lt;em&gt;this&lt;/em&gt; codebase uses queues, middleware, auth, caching, tenancy, validation, or exception handling.&lt;/p&gt;

&lt;p&gt;That distinction changed how I run upgrades now: AI helps me audit the change set, but I do not let it pretend to own the migration. The architectural call still needs a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI Review Actually Helps
&lt;/h2&gt;

&lt;p&gt;The best use of AI in a Laravel upgrade is narrow and mechanical. Give it a diff, the target Laravel version, and a concrete question. That tends to produce useful output quickly.&lt;/p&gt;

&lt;p&gt;In my experience, AI is good at spotting four classes of issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework API drift&lt;/strong&gt;: deprecated helpers, signature changes, config keys that moved, middleware registration changes, or updated bootstrapping conventions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern mismatch&lt;/strong&gt;: places where half the app uses the new pattern and the other half still uses the old one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade guide coverage gaps&lt;/strong&gt;: areas you skipped because the app still boots, but the framework now expects a cleaner or safer implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-level review fatigue&lt;/strong&gt;: dozens of tiny edits that are individually obvious but easy to miss when you are tired.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matters because upgrade work creates a lot of noise. If you are moving from one Laravel major version to another, the official upgrade guide is essential, but it does not tell you where your specific codebase is fragile. It tells you what changed in the framework. You still need to map that onto your app.&lt;/p&gt;

&lt;p&gt;The official docs are still the anchor here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://laravel.com/docs/upgrade" rel="noopener noreferrer"&gt;Laravel upgrade guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://laravel.com/docs/releases" rel="noopener noreferrer"&gt;Laravel releases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI becomes valuable when you already know the target and want a fast consistency scan. It is much less valuable when you want it to infer business intent from a codebase it met thirty seconds ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bugs AI Usually Misses
&lt;/h2&gt;

&lt;p&gt;The dangerous part of AI review is not that it is dumb. It is that it is &lt;strong&gt;plausible&lt;/strong&gt;. It often produces the kind of answer that sounds senior enough to pass a quick read, while still being wrong in the places that matter.&lt;/p&gt;

&lt;p&gt;During upgrades, the misses usually fall into three buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  It does not understand your invariants
&lt;/h3&gt;

&lt;p&gt;Laravel gives you abstractions. Your application gives those abstractions meaning. A queue job in one project is a harmless background sync. In another, it is part of a payment pipeline with strict ordering guarantees. A middleware change can look cosmetic until it silently alters tenant resolution or auth context.&lt;/p&gt;

&lt;p&gt;AI can say, "this code should use the newer registration style," and still miss that the current order exists to preserve behavior around impersonation, locale resolution, or request-scoped caching.&lt;/p&gt;

&lt;p&gt;That is why upgrade bugs often show up in places that look boring in the diff. The framework changed something generic. Your app depended on the old behavior in a very non-generic way.&lt;/p&gt;

&lt;h3&gt;
  
  
  It tends to normalize toward framework defaults
&lt;/h3&gt;

&lt;p&gt;This is one of the biggest traps. AI usually assumes your code should look more like the framework docs. Sometimes that is correct. Sometimes your app intentionally deviates because the default is wrong for your domain.&lt;/p&gt;

&lt;p&gt;I have seen this show up in exception rendering, validation flow, broadcast auth, guard selection, and database transaction boundaries. The AI recommendation looks clean because it moves the app closer to "standard Laravel." But standard Laravel is not automatically correct Laravel.&lt;/p&gt;

&lt;h3&gt;
  
  
  It cannot rank risk the way a maintainer can
&lt;/h3&gt;

&lt;p&gt;Upgrade work is not just about correctness. It is about sequencing. Which change is safe now? Which one should be isolated? Which one needs a feature flag? Which one needs product signoff because it changes observable behavior?&lt;/p&gt;

&lt;p&gt;AI review is weak at that layer. It can tell you what is different. It usually cannot tell you which difference is worth waking up for at 2 AM.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed in My Upgrade Process
&lt;/h2&gt;

&lt;p&gt;The useful shift was simple: I stopped treating AI as a reviewer of the whole upgrade and started treating it as a reviewer of &lt;strong&gt;prepared evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means I now structure the upgrade before I ask AI to look at anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  I keep an upgrade notes file
&lt;/h3&gt;

&lt;p&gt;Before changing code, I write down the target version, the official upgrade notes I expect to touch, risky subsystems, and known app-specific deviations. This dramatically improves both human review and AI review, because the work has context.&lt;/p&gt;

&lt;p&gt;A stripped-down version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Laravel 12 Upgrade Notes&lt;/span&gt;

&lt;span class="gu"&gt;### Expected framework touchpoints&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; bootstrap / app configuration
&lt;span class="p"&gt;-&lt;/span&gt; exception handling
&lt;span class="p"&gt;-&lt;/span&gt; middleware registration
&lt;span class="p"&gt;-&lt;/span&gt; queue + scheduler behavior
&lt;span class="p"&gt;-&lt;/span&gt; auth / guards
&lt;span class="p"&gt;-&lt;/span&gt; validation and request objects

&lt;span class="gu"&gt;### App-specific risk areas&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; tenant resolution depends on middleware order
&lt;span class="p"&gt;-&lt;/span&gt; admin guard differs from default web guard
&lt;span class="p"&gt;-&lt;/span&gt; payment jobs rely on serialized DTO shape
&lt;span class="p"&gt;-&lt;/span&gt; API clients retry through custom exception mapping

&lt;span class="gu"&gt;### Non-goals&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; no opportunistic refactors
&lt;span class="p"&gt;-&lt;/span&gt; no config cleanup unrelated to upgrade
&lt;span class="p"&gt;-&lt;/span&gt; no auth redesign during this PR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not documentation theater. It forces scope control. It also makes bad AI suggestions easier to reject, because you already wrote down the constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  I separate mechanical changes from behavior changes
&lt;/h3&gt;

&lt;p&gt;If an upgrade PR mixes signature updates, container changes, config rewrites, auth cleanup, and test rewrites, the review quality collapses. AI becomes noisier and humans become less reliable.&lt;/p&gt;

&lt;p&gt;So I split the work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mechanical compatibility changes go first.&lt;/li&gt;
&lt;li&gt;Behavior-preserving test updates come next.&lt;/li&gt;
&lt;li&gt;Architectural changes happen only if the upgrade genuinely requires them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation matters because AI is strongest in the first category and weakest in the third.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: A Change That Looks Safe but Is Not
&lt;/h2&gt;

&lt;p&gt;A common upgrade trap is middleware or bootstrap registration. Laravel evolves how the application is configured, and AI will often recommend moving everything to the modern pattern immediately. Sometimes that is fine. Sometimes it breaks assumptions hidden in execution order.&lt;/p&gt;

&lt;p&gt;Here is the kind of thing that deserves human attention:&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'tenant'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Http\Middleware\ResolveTenant&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="s1"&gt;'admin'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Http\Middleware\RequireAdmin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;appendToGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'web'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;\App\Http\Middleware\ResolveTenant&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="nc"&gt;\App\Http\Middleware\ApplyTenantLocale&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An AI review may say this is fine, or suggest a cleaner registration style. The real question is different: &lt;strong&gt;what depends on that order?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;ResolveTenant&lt;/code&gt; used to run earlier through a previous kernel arrangement, moving it without checking can break:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tenant-aware route model binding&lt;/li&gt;
&lt;li&gt;locale selection before validation messages are built&lt;/li&gt;
&lt;li&gt;per-tenant cache prefixes&lt;/li&gt;
&lt;li&gt;auth guard resolution for admin subdomains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is obvious from the framework diff alone. You need application context, and you need tests that prove the contract.&lt;/p&gt;

&lt;p&gt;The right follow-up is not "does this match the docs?" It is "what user-visible behavior did this ordering previously guarantee?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests Are the Real Counterweight
&lt;/h2&gt;

&lt;p&gt;If you want AI review to be useful during upgrades, give it a codebase with strong regression tests. Otherwise you are asking a language model to do architecture and QA at the same time, which is where the fantasy starts.&lt;/p&gt;

&lt;p&gt;I now treat tests as the primary control system and AI as a secondary scanner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write tests around the risky edges first
&lt;/h3&gt;

&lt;p&gt;Before or during the upgrade, I want tests around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request lifecycle assumptions&lt;/li&gt;
&lt;li&gt;auth and permission boundaries&lt;/li&gt;
&lt;li&gt;queue serialization and retries&lt;/li&gt;
&lt;li&gt;exception-to-response mapping&lt;/li&gt;
&lt;li&gt;integration points with external services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if your app depends on a custom exception becoming a specific JSON error shape, lock that down explicitly:&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'maps billing exceptions to a stable API response'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="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;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\App\Services\BillingGateway&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="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;'charge'&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;andThrow&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;\App\Exceptions\BillingDeclined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Card declined'&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;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/checkout'&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'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'pro'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'token'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'tok_test'&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;402&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;assertJson&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;'Payment could not be processed.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'code'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'billing_declined'&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;That test does more for upgrade safety than five pages of AI commentary. It preserves the contract that matters.&lt;/p&gt;

&lt;p&gt;Once those tests exist, AI becomes more useful because it can help identify other places where similar assumptions may have drifted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use AI to ask targeted test questions
&lt;/h3&gt;

&lt;p&gt;This is where the workflow starts to work well. Instead of asking, "review my Laravel upgrade," ask narrower questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which touched areas lack regression tests?&lt;/li&gt;
&lt;li&gt;Which renamed methods or config shifts appear incomplete?&lt;/li&gt;
&lt;li&gt;Which custom exceptions, guards, or jobs are likely sensitive to framework lifecycle changes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That framing keeps AI in the lane where it adds leverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human Review Still Owns the Last Mile
&lt;/h2&gt;

&lt;p&gt;The final review on an upgrade should not be a generic "LGTM" pass. It should be a risk review by someone who understands both Laravel and the business behavior behind the app.&lt;/p&gt;

&lt;p&gt;What I care about in that last pass is not whether the code looks modern. I care about whether the upgrade preserved the contracts we actually rely on.&lt;/p&gt;

&lt;p&gt;A useful human review usually asks questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did we change behavior, or only compatibility?&lt;/li&gt;
&lt;li&gt;Did any framework-default recommendation override an intentional local design?&lt;/li&gt;
&lt;li&gt;Do the tests cover the scary paths, not just the happy paths?&lt;/li&gt;
&lt;li&gt;Did we quietly mix upgrade work with cleanup work that should have been separate?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters more than teams admit. Upgrade PRs get dangerous when they become an excuse to tidy architecture. Cleanups feel efficient in the moment, but they destroy your ability to isolate regressions.&lt;/p&gt;

&lt;p&gt;My rule now is blunt: &lt;strong&gt;if a change is not required for the upgrade, it needs a stronger reason than "we were already in the file."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Rule I Use Now
&lt;/h2&gt;

&lt;p&gt;AI review is worth using in Laravel upgrades, but only after you define the problem properly. It is a strong second pass for mechanical drift, incomplete migrations, and consistency checks. It is a weak substitute for architectural judgment, regression strategy, and knowledge of why your app is weird in the first place.&lt;/p&gt;

&lt;p&gt;So the process I trust looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the official Laravel upgrade notes.&lt;/li&gt;
&lt;li&gt;Write app-specific upgrade notes before touching code.&lt;/li&gt;
&lt;li&gt;Split mechanical edits from behavioral changes.&lt;/li&gt;
&lt;li&gt;Lock down risky behavior with tests.&lt;/li&gt;
&lt;li&gt;Use AI for targeted review, not for ownership.&lt;/li&gt;
&lt;li&gt;End with human review focused on invariants and risk.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you only remember one thing, make it this: &lt;strong&gt;AI can help you finish a Laravel upgrade faster, but it cannot tell you what your application is allowed to break.&lt;/strong&gt; That call is still yours, and pretending otherwise is how "successful" upgrades ship regressions.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/ai-review-for-laravel-upgrades-is-useful-but-not-enough/" rel="noopener noreferrer"&gt;https://qcode.in/ai-review-for-laravel-upgrades-is-useful-but-not-enough/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I’d Use AI-Written Code Without Losing Touch With the Codebase</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Mon, 10 Aug 2026 06:08:12 +0000</pubDate>
      <link>https://dev.to/saqueib/how-id-use-ai-written-code-without-losing-touch-with-the-codebase-1l1a</link>
      <guid>https://dev.to/saqueib/how-id-use-ai-written-code-without-losing-touch-with-the-codebase-1l1a</guid>
      <description>&lt;p&gt;Shipping AI-written code is not dangerous because the model makes syntax mistakes. That part is easy to catch. The real risk is quieter: your team merges working code that nobody fully owns anymore. The code passes, the feature ships, and six weeks later a small change turns into a forensic exercise because the engineers who approved it never built a real mental model of it.&lt;/p&gt;

&lt;p&gt;That is &lt;strong&gt;cognitive debt&lt;/strong&gt;. It compounds faster than technical debt because it attacks the thing teams rely on to pay technical debt down later: understanding.&lt;/p&gt;

&lt;p&gt;If you use &lt;strong&gt;Claude Code&lt;/strong&gt;, &lt;strong&gt;Codex&lt;/strong&gt;, or &lt;strong&gt;Cursor&lt;/strong&gt;, the answer is not to ban them. The answer is to tighten the loop between generation and comprehension. Teams that keep ownership do a few things differently: they review for mechanism instead of style, they selectively retype critical paths, they force architecture checkpoints, and they prompt from tests instead of vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cognitive Debt Starts When Reading Replaces Thinking
&lt;/h2&gt;

&lt;p&gt;Most teams notice the wrong failure mode first. They worry that AI will generate bad code. In practice, modern coding agents often generate code that is superficially fine: clean naming, decent structure, passing tests, maybe even better formatting than the average rushed engineer.&lt;/p&gt;

&lt;p&gt;The problem starts when engineers become &lt;strong&gt;operators of generation&lt;/strong&gt; instead of &lt;strong&gt;authors of systems&lt;/strong&gt;. If your interaction pattern is "describe task, accept diff, skim output, merge," you are outsourcing more than typing. You are outsourcing the chain of reasoning that usually builds architectural memory.&lt;/p&gt;

&lt;p&gt;That missing reasoning shows up later in predictable ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small edits feel riskier than they should.&lt;/li&gt;
&lt;li&gt;Review comments drift toward formatting instead of behavior.&lt;/li&gt;
&lt;li&gt;Engineers trust tests they did not design.&lt;/li&gt;
&lt;li&gt;Bugs take longer to localize because nobody knows which assumptions were deliberate.&lt;/li&gt;
&lt;li&gt;Refactors stall because the team remembers the surface, not the structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful rule: &lt;strong&gt;if your team cannot explain why a generated implementation is shaped this way instead of two nearby alternatives, you already took on cognitive debt&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is why this is not a style problem. It is an ownership problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review Rituals Need To Target Understanding, Not Polish
&lt;/h2&gt;

&lt;p&gt;Traditional code review habits are too weak for agent-generated code. A quick skim might be enough for a human-written patch from a trusted teammate because the author likely carried intent through the work. With AI-written code, the patch may be coherent while the reasoning behind it is thin, inconsistent, or completely absent.&lt;/p&gt;

&lt;p&gt;So the review ritual needs a stronger bar. Not heavier process. Better questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ask mechanism questions
&lt;/h3&gt;

&lt;p&gt;A useful review comment is not "can we rename this helper?" A useful review comment is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why is state derived here instead of at the boundary?&lt;/li&gt;
&lt;li&gt;What invariant is this cache relying on?&lt;/li&gt;
&lt;li&gt;What breaks if this async step resolves twice?&lt;/li&gt;
&lt;li&gt;Why is this controller validating and mapping instead of handing off to an action or service?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions force the reviewer to reconstruct the design. If they cannot, the patch is not ready, even if it is technically correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Require an implementation note on non-trivial diffs
&lt;/h3&gt;

&lt;p&gt;For anything with concurrency, persistence, caching, authorization, background jobs, or cross-service effects, require a short note from the engineer driving the agent. Not a novel. Just enough to prove they own the shape of the code.&lt;/p&gt;

&lt;p&gt;A good note looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Implementation note:
&lt;span class="p"&gt;-&lt;/span&gt; Validation stays at the HTTP boundary.
&lt;span class="p"&gt;-&lt;/span&gt; Domain mapping happens in OrderDraftFactory so jobs and controllers share one path.
&lt;span class="p"&gt;-&lt;/span&gt; Idempotency is enforced with a unique database key on external_event_id.
&lt;span class="p"&gt;-&lt;/span&gt; Retry safety matters more here than raw throughput.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That note does two things. It gives reviewers real hooks, and it forces the engineer to collapse the generated output into a human model before merge.&lt;/p&gt;

&lt;p&gt;If your team skips this step, review becomes theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retype The Parts That Carry System Meaning
&lt;/h2&gt;

&lt;p&gt;The most underrated defense against cognitive debt is &lt;strong&gt;selective retyping&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not whole files. Not busywork. Just the sections that encode the system's actual decisions.&lt;/p&gt;

&lt;p&gt;When people hear this, they usually object that retyping wastes the speed benefit. That is the wrong optimization target. The scarce resource is not keystrokes. It is &lt;strong&gt;understanding at the moment of change&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should be retyped
&lt;/h3&gt;

&lt;p&gt;Retype code when it defines one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core domain rules&lt;/li&gt;
&lt;li&gt;Transaction boundaries&lt;/li&gt;
&lt;li&gt;Query composition with subtle filters or joins&lt;/li&gt;
&lt;li&gt;Authorization logic&lt;/li&gt;
&lt;li&gt;Retry or idempotency behavior&lt;/li&gt;
&lt;li&gt;Data transformations that downstream systems depend on&lt;/li&gt;
&lt;li&gt;Prompt construction for agent workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are the places where hand contact matters. Retyping slows you down just enough to notice when something is off, overly clever, or based on a bad assumption.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should not be retyped
&lt;/h3&gt;

&lt;p&gt;Do not fetishize manual coding. Let the tool write the boring parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DTOs and basic schemas&lt;/li&gt;
&lt;li&gt;Repetitive CRUD plumbing&lt;/li&gt;
&lt;li&gt;Test fixture setup&lt;/li&gt;
&lt;li&gt;Mechanical refactors&lt;/li&gt;
&lt;li&gt;Simple adapters with obvious behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not purity. The point is to keep engineers mentally attached to the code that shapes behavior.&lt;/p&gt;

&lt;p&gt;A practical team rule is this: &lt;strong&gt;accept generated scaffolding freely, but manually rewrite the decision-making core&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put Architecture Checkpoints Before Merge, Not After Incidents
&lt;/h2&gt;

&lt;p&gt;AI tools are good at making local decisions look finished. That is exactly why they need explicit architecture checkpoints. Without them, you end up approving code that works in isolation but pushes complexity into the wrong layer.&lt;/p&gt;

&lt;p&gt;In Laravel and full stack codebases, this usually shows up as controller bloat, duplicated orchestration logic, weak domain boundaries, and tests that verify current implementation instead of intended behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  A simple checkpoint template
&lt;/h3&gt;

&lt;p&gt;Before merging a non-trivial AI-assisted patch, answer these five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where does validation belong?&lt;/li&gt;
&lt;li&gt;Where does orchestration belong?&lt;/li&gt;
&lt;li&gt;What is the stable domain boundary here?&lt;/li&gt;
&lt;li&gt;Which part must be idempotent or retry-safe?&lt;/li&gt;
&lt;li&gt;What would be painful to change in three months?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If those answers are fuzzy, stop generating more code. The design is not ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: thin controller vs generated sprawl
&lt;/h3&gt;

&lt;p&gt;This is the kind of thing agents often produce when you prompt too broadly:&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="nv"&gt;$validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'email'&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;'required'&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="s1"&gt;'plan'&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;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&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;firstOrCreate&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="nv"&gt;$validated&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="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasStripeId&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createAsStripeCustomer&lt;/span&gt;&lt;span class="p"&gt;();&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="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;newSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$validated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'plan'&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="nc"&gt;AuditLog&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;'event'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'subscription_created'&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="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="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;SendWelcomeSequence&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;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;'subscription_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="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;It works. It is also carrying validation, user creation policy, billing orchestration, audit logging, and side effects in one HTTP action. That is not a controller. That is a future maintenance problem.&lt;/p&gt;

&lt;p&gt;A tighter version is not just prettier. It has clearer ownership:&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;CreateSubscriptionRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;CreateSubscription&lt;/span&gt; &lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="p"&gt;)&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="nv"&gt;$action&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$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="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;'subscription_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="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;Now the real logic lives in a named action with explicit tests. The agent can still help write it, but the architecture has a spine.&lt;/p&gt;

&lt;p&gt;That is the distinction that matters: &lt;strong&gt;generated code should fill a design, not invent one silently inside a diff&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt From Tests First, Then Let The Agent Fill The Gaps
&lt;/h2&gt;

&lt;p&gt;If you want less cognitive debt, stop starting with "build feature X." Start with tests and constraints.&lt;/p&gt;

&lt;p&gt;A vague prompt gives you fast code and weak ownership. A test-first prompt gives you slower output up front and much stronger control over behavior, interfaces, and failure modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better prompt shape
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build a webhook handler for payment events and make it production ready.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write Pest tests first for a Laravel webhook handler.
Constraints:
- Must be idempotent on provider event ID.
- Invalid signatures return 400 and never enqueue jobs.
- Processing should happen in an action class, not the controller.
- Retries must be safe.
- Use a fake event payload factory in tests.
After the tests, implement the minimal code to pass them.
Then explain the chosen boundaries in 5 bullets.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prompt does three important things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It defines failure modes before implementation.&lt;/li&gt;
&lt;li&gt;It constrains architecture instead of asking the model to guess it.&lt;/li&gt;
&lt;li&gt;It forces the agent to produce an explanation you can review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern works across stacks. For frontend work, define rendering states and interaction tests first. For backend jobs, define retry semantics and side effects first. For agent workflows, define tool contracts and recovery behavior first.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hidden win
&lt;/h3&gt;

&lt;p&gt;Test-first prompting is not only about correctness. It gives the engineer a &lt;strong&gt;better memory trace&lt;/strong&gt;. Writing or reviewing tests first creates a narrative of expected behavior. That narrative survives longer than whatever generated implementation happened to satisfy it this week.&lt;/p&gt;

&lt;p&gt;Teams that skip this usually end up with the opposite: good-looking code and shallow recall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat AI Output Like Pair Programming With A Fast, Forgetful Junior
&lt;/h2&gt;

&lt;p&gt;The healthiest mental model is not "AI writes code for me." It is "I am pair programming with someone fast, confident, and unreliable about consequences."&lt;/p&gt;

&lt;p&gt;That framing changes how you work.&lt;/p&gt;

&lt;p&gt;You do not hand the junior a vague ticket and merge whatever comes back. You set boundaries, inspect decisions, rewrite critical sections, and insist on tests where behavior matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  A lightweight operating policy
&lt;/h3&gt;

&lt;p&gt;If you want something your team can actually adopt next week, start here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use AI freely for scaffolding, repetition, and search-heavy edits.&lt;/li&gt;
&lt;li&gt;Require test-first or constraint-first prompts for non-trivial work.&lt;/li&gt;
&lt;li&gt;Require a short implementation note on risky diffs.&lt;/li&gt;
&lt;li&gt;Manually rewrite the parts that encode business rules or system boundaries.&lt;/li&gt;
&lt;li&gt;Reject reviews that only discuss formatting and naming.&lt;/li&gt;
&lt;li&gt;Track bugs caused by misunderstood generated code separately from normal defects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters. If you do not measure cognitive misses, the team will keep telling itself velocity is fine while ownership is decaying underneath.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to watch for in real teams
&lt;/h3&gt;

&lt;p&gt;The strongest warning signs are cultural, not technical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Engineers say "the agent did that" as if authorship transferred.&lt;/li&gt;
&lt;li&gt;People hesitate to touch recently generated modules.&lt;/li&gt;
&lt;li&gt;The same reviewer approves every AI-heavy diff because others do not want to unravel it.&lt;/li&gt;
&lt;li&gt;Post-merge fixes cluster around misunderstood assumptions rather than hard edge cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When those patterns appear, your process is rewarding throughput at the expense of comprehension.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Rule
&lt;/h2&gt;

&lt;p&gt;Use AI to remove typing, not to remove thinking.&lt;/p&gt;

&lt;p&gt;That is the line. If a workflow makes your team faster &lt;strong&gt;and&lt;/strong&gt; preserves a clear human explanation of behavior, boundaries, and tradeoffs, keep it. If it produces passing code that nobody can confidently reshape a month later, it is too expensive, no matter how good the demo looks.&lt;/p&gt;

&lt;p&gt;The practical rule is simple: &lt;strong&gt;generate broadly, review mechanically, rewrite selectively, and anchor everything in tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That combination keeps the leverage and avoids the trap. Without it, AI-written code does not just add technical debt. It slowly teaches your team to stop holding the system in their heads, and once that habit lands, the codebase gets harder every sprint.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/how-to-avoid-cognitive-debt-when-shipping-ai-written-code/" rel="noopener noreferrer"&gt;https://qcode.in/how-to-avoid-cognitive-debt-when-shipping-ai-written-code/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>coding</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>Laravel AI Search: When SQL Wins, When Embeddings Help, and When to Use Hybrid</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Sun, 09 Aug 2026 05:40:20 +0000</pubDate>
      <link>https://dev.to/saqueib/laravel-ai-search-when-sql-wins-when-embeddings-help-and-when-to-use-hybrid-3kof</link>
      <guid>https://dev.to/saqueib/laravel-ai-search-when-sql-wins-when-embeddings-help-and-when-to-use-hybrid-3kof</guid>
      <description>&lt;p&gt;Laravel teams keep reaching for embeddings the moment a product manager says “AI search.” That is usually the wrong first move. &lt;strong&gt;Most Laravel apps should start with solid SQL search, add vectors only when language mismatch becomes the real problem, and use hybrid retrieval when the stakes justify the complexity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That recommendation sounds conservative, but it saves time, money, and a lot of relevance debugging. Search quality is not decided by whichever retrieval method sounds smarter. It is decided by whether your index matches the user’s intent, whether your ranking is inspectable, and whether your team can actually maintain the system in production.&lt;/p&gt;

&lt;p&gt;If you are building search in a Laravel app today, the real question is not “Should we use embeddings?” It is: &lt;strong&gt;what failure mode are we solving?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SQL Still Wins More Than People Admit
&lt;/h2&gt;

&lt;p&gt;For many Laravel products, search is still dominated by exact terms, field weighting, filters, recency, and business rules. That is classic relational territory.&lt;/p&gt;

&lt;p&gt;If a user searches for an invoice number, a customer email, a SKU, a job title, a plan name, or a feature flag, vector search adds very little. In fact, it often makes those cases worse because semantic similarity is not the same as precision.&lt;/p&gt;

&lt;p&gt;A normal SQL-backed approach can go a long way when you structure it properly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;normalize searchable text&lt;/li&gt;
&lt;li&gt;use full-text indexes where your database supports them&lt;/li&gt;
&lt;li&gt;mix exact-match boosts with partial-match fallbacks&lt;/li&gt;
&lt;li&gt;rank by business value, not just textual similarity&lt;/li&gt;
&lt;li&gt;keep filters first-class instead of bolting them on later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Laravel, this baseline is usually faster to ship and much easier to debug than a vector stack.&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;$query&lt;/span&gt; &lt;span class="o"&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;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&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;selectRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"(
            CASE WHEN slug = ? THEN 100 ELSE 0 END +
            CASE WHEN title LIKE ? THEN 40 ELSE 0 END +
            CASE WHEN MATCH(title, excerpt, body) AGAINST (? IN NATURAL LANGUAGE MODE)
                THEN MATCH(title, excerpt, body) AGAINST (? IN NATURAL LANGUAGE MODE)
                ELSE 0
            END +
            CASE WHEN published_at &amp;gt;= NOW() - INTERVAL 90 DAY THEN 5 ELSE 0 END
        ) AS relevance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$query&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'published'&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;having&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'relevance'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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;orderByDesc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'relevance'&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;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not glamorous, but it is operationally honest. You can explain why a record ranked well. You can tweak weights. You can add hard constraints. You can log bad queries and improve them quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where SQL Search Breaks
&lt;/h3&gt;

&lt;p&gt;The limit shows up when the query and the document use different language.&lt;/p&gt;

&lt;p&gt;A user searches for “cancel subscription,” but your help article says “terminate billing cycle.” A recruiter searches for “backend lead,” but the profile says “principal platform engineer.” A support agent types “login loop after 2FA” and the incident summary uses different wording entirely.&lt;/p&gt;

&lt;p&gt;That is where plain keyword retrieval starts leaking relevance. The problem is no longer indexing or filters. It is &lt;strong&gt;semantic mismatch&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embeddings Solve a Different Problem
&lt;/h2&gt;

&lt;p&gt;Embeddings are useful when you need meaning-based retrieval, not just text matching. They work best when your content is long-form, language-rich, and likely to be queried with varied phrasing.&lt;/p&gt;

&lt;p&gt;Good candidates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;help centers and internal docs&lt;/li&gt;
&lt;li&gt;policy libraries&lt;/li&gt;
&lt;li&gt;knowledge bases for AI assistants&lt;/li&gt;
&lt;li&gt;long ticket histories or call summaries&lt;/li&gt;
&lt;li&gt;product documentation with many paraphrased concepts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad candidates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;admin tables&lt;/li&gt;
&lt;li&gt;SKUs and IDs&lt;/li&gt;
&lt;li&gt;invoice search&lt;/li&gt;
&lt;li&gt;faceted catalog browsing&lt;/li&gt;
&lt;li&gt;highly structured operational data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction matters because vector search comes with real cost. You do not just store embeddings and move on. You now own chunking strategy, model choice, re-embedding pipelines, vector index storage, latency budgets, and failure analysis when the search starts returning “kind of related” junk.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hidden Cost Is Relevance Debugging
&lt;/h3&gt;

&lt;p&gt;SQL search fails loudly. Vector search often fails softly.&lt;/p&gt;

&lt;p&gt;With SQL, you can usually tell why a record did not match. With embeddings, the failure mode is murkier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the chunk boundaries were wrong&lt;/li&gt;
&lt;li&gt;the wrong field was embedded&lt;/li&gt;
&lt;li&gt;the document was too broad&lt;/li&gt;
&lt;li&gt;the query embedding drifted semantically&lt;/li&gt;
&lt;li&gt;the nearest neighbors were technically related but practically useless&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes embeddings powerful, but not automatically developer-friendly.&lt;/p&gt;

&lt;p&gt;In a Laravel app, a typical vector flow might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EmbeddingClient&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;embed&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;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'knowledge_chunks'&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;select&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;'document_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'chunk_text'&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;selectRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 - (embedding &amp;lt;=&amp;gt; ?) as similarity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$embedding&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'workspace_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$workspaceId&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;orderByRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'embedding &amp;lt;=&amp;gt; ?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$embedding&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;limit&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The syntax depends on your storage choice, but the point is the same: this system is only as good as the chunks, the embedding model, and the retrieval constraints around it.&lt;/p&gt;

&lt;p&gt;If you skip that design work, vector search turns into an expensive fuzzy lookup with impressive demos and disappointing production behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid Retrieval Is Usually the Adult Answer
&lt;/h2&gt;

&lt;p&gt;If your Laravel app has both structured precision needs and language-heavy discovery needs, &lt;strong&gt;hybrid retrieval is the best default end state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Hybrid does not mean “throw SQL and vectors into one bag and hope.” It means each retrieval method does what it is good at, and your ranking layer combines them intentionally.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;run a keyword or full-text query&lt;/li&gt;
&lt;li&gt;run a vector query&lt;/li&gt;
&lt;li&gt;merge candidates&lt;/li&gt;
&lt;li&gt;rerank using business rules, exact-match boosts, and optionally an LLM or cross-encoder&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives you the best shot at balancing precision and recall.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Practical Laravel Shape
&lt;/h3&gt;

&lt;p&gt;In practice, you might keep two indexes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a relational or full-text path for exact terms, filters, recency, and field-aware ranking&lt;/li&gt;
&lt;li&gt;a vector path for semantic recall across longer content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then combine the result sets in application code.&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;$keywordResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KeywordSearch&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$vectorResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VectorSearch&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$keywordResults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$vectorResults&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;flatten&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;groupBy&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$group&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$keywordScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$group&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'keyword_score'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$vectorScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$group&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'vector_score'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$freshnessBoost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gt&lt;/span&gt;&lt;span class="p"&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;subDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$exactTitleBoost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;exact_title_match&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;final_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$keywordScore&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$vectorScore&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
            &lt;span class="nv"&gt;$freshnessBoost&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
            &lt;span class="nv"&gt;$exactTitleBoost&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;$item&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;sortByDesc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'final_score'&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;take&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;values&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 not mathematically pure, and that is fine. Search relevance in production is rarely elegant. What matters is that the ranking logic stays legible and adjustable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Hybrid Actually Pays Off
&lt;/h3&gt;

&lt;p&gt;Hybrid retrieval is worth the overhead when all of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;users search with messy, natural language&lt;/li&gt;
&lt;li&gt;exact terms still matter in part of the ranking&lt;/li&gt;
&lt;li&gt;content is large enough that missed recall hurts real workflows&lt;/li&gt;
&lt;li&gt;you have enough search volume or business value to justify tuning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A help center with AI chat grounding is a strong example. Users may search with vague phrasing, but exact product names, feature flags, plan tiers, and error codes still matter. Pure SQL misses semantic matches. Pure vector search can bury the exact answer. Hybrid gives you a better operating envelope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indexing Cost Changes the Decision More Than Most Teams Expect
&lt;/h2&gt;

&lt;p&gt;Retrieval quality is only half the story. The other half is &lt;strong&gt;what it costs to keep the index correct&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SQL search has the lowest maintenance burden because the source of truth and the searchable representation usually live close together. Updates are straightforward. Reindexing is familiar. Operational ownership stays with the same team that owns the app.&lt;/p&gt;

&lt;p&gt;Embeddings change that equation.&lt;/p&gt;

&lt;p&gt;Every meaningful content change may require re-embedding. If you store chunked documents, you also need deterministic chunking so updates do not invalidate your ranking behavior unpredictably. If you switch embedding models later, you may need a full backfill. If you support multi-tenant search, index growth becomes a real budget line, not an implementation detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Useful Decision Rule
&lt;/h3&gt;

&lt;p&gt;Ask three questions before introducing vectors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the current failure due to poor ranking or poor semantic recall?&lt;/li&gt;
&lt;li&gt;Can we measure the missed cases with real queries?&lt;/li&gt;
&lt;li&gt;Do we have an indexing pipeline we trust in production?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer to the third question is no, do not pretend embeddings are a drop-in upgrade. They are an infrastructure choice.&lt;/p&gt;

&lt;p&gt;For Laravel teams, this often means building a queue-backed indexing pipeline before going live with semantic retrieval.&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;SyncKnowledgeChunkEmbeddings&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;__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;$documentId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;EmbeddingClient&lt;/span&gt; &lt;span class="nv"&gt;$embeddings&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;$document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KnowledgeDocument&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;findOrFail&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;documentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DocumentChunker&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$document&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;KnowledgeChunk&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'document_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$document&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chunks&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$index&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$chunkText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;KnowledgeChunk&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;'document_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$document&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;'chunk_index'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'chunk_text'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$chunkText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'embedding'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$embeddings&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chunkText&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part people skip in architecture diagrams. The retrieval demo is easy. The indexing lifecycle is where teams either build a real system or accumulate search debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relevance Tuning: Which System Can Your Team Actually Operate?
&lt;/h2&gt;

&lt;p&gt;This is where simple SQL keeps surprising people. It is not that embeddings are weak. It is that &lt;strong&gt;keyword ranking is often easier for a product team to improve week after week&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When stakeholders complain that the wrong result shows first, you need a tuning loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect the query&lt;/li&gt;
&lt;li&gt;inspect the candidates&lt;/li&gt;
&lt;li&gt;explain the ranking&lt;/li&gt;
&lt;li&gt;change something small&lt;/li&gt;
&lt;li&gt;measure whether it helped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That loop is much tighter in SQL and moderately harder in hybrid systems. It is hardest in pure vector systems unless you invest in evaluation tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQL Is Better for High-Control Search
&lt;/h3&gt;

&lt;p&gt;If your product needs deterministic ranking rules, SQL or full-text should remain the backbone. Think CRMs, dashboards, admin panels, marketplaces, internal tools, and anything with dense filters.&lt;/p&gt;

&lt;p&gt;In these systems, users are not asking broad conceptual questions. They are trying to find the right record under constraints. Exactness beats cleverness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embeddings Win for Knowledge Retrieval
&lt;/h3&gt;

&lt;p&gt;If your app is powering assistant responses, internal documentation lookup, or support knowledge discovery, semantic retrieval earns its keep. But even there, I would still avoid pure vector search unless the corpus is truly narrative and low on structured signals.&lt;/p&gt;

&lt;p&gt;Most production systems need metadata filters, source weighting, freshness bias, and sometimes document-type boosts. That already pushes you toward hybrid thinking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Ship in a Real Laravel Codebase
&lt;/h2&gt;

&lt;p&gt;If I were building this today, I would not start with the most advanced stack. I would stage the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: Strong SQL Baseline
&lt;/h3&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exact-match boosts on key identifiers and titles&lt;/li&gt;
&lt;li&gt;full-text search on main body fields&lt;/li&gt;
&lt;li&gt;explicit filters for tenant, status, visibility, and type&lt;/li&gt;
&lt;li&gt;logging for low-result or zero-result queries&lt;/li&gt;
&lt;li&gt;admin tooling to inspect rankings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gets you a reliable baseline quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Add Vectors Only Where Recall Is Failing
&lt;/h3&gt;

&lt;p&gt;Do not embed everything. Pick the content types where phrasing mismatch is clearly hurting outcomes. Usually that means docs, notes, transcripts, or tickets. Keep relational search for structured entities.&lt;/p&gt;

&lt;p&gt;That split is cleaner architecturally and cheaper operationally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: Merge Into Hybrid Retrieval
&lt;/h3&gt;

&lt;p&gt;Once you have evidence from real search logs, merge both paths and tune the reranking layer. This is where you add the product-specific intelligence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exact title or slug boosts&lt;/li&gt;
&lt;li&gt;newer content boosts&lt;/li&gt;
&lt;li&gt;authoritative source boosts&lt;/li&gt;
&lt;li&gt;demotion of thin or duplicate chunks&lt;/li&gt;
&lt;li&gt;optional reranking for top candidates only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters. If you want to use a reranker or LLM judge, use it on a small candidate set. Do not waste expensive inference on your whole corpus.&lt;/p&gt;

&lt;p&gt;For Laravel teams, this staged approach is the difference between “we shipped useful search” and “we adopted search infrastructure.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sharp Recommendation
&lt;/h2&gt;

&lt;p&gt;If your Laravel app mainly searches records, products, users, tickets, or operational data, &lt;strong&gt;start with SQL and full-text search&lt;/strong&gt;. It is cheaper, easier to tune, and usually more correct.&lt;/p&gt;

&lt;p&gt;If your app searches long-form knowledge where users describe ideas in inconsistent language, &lt;strong&gt;add embeddings&lt;/strong&gt;. But treat them as a semantic recall layer, not magic relevance dust.&lt;/p&gt;

&lt;p&gt;If both worlds matter, &lt;strong&gt;ship hybrid retrieval&lt;/strong&gt;. That is the pragmatic production answer for most serious AI-enabled apps.&lt;/p&gt;

&lt;p&gt;The mistake is not choosing the “wrong” algorithm. The mistake is solving a semantic problem with business-rule tools, or solving a precision problem with semantic tools.&lt;/p&gt;

&lt;p&gt;Use SQL when you need control. Use embeddings when you need meaning. Use hybrid when your users need both. That is the version that survives contact with production.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/laravel-ai-search-embeddings-sql-hybrid-retrieval/" rel="noopener noreferrer"&gt;https://qcode.in/laravel-ai-search-embeddings-sql-hybrid-retrieval/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>search</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Laravel cooldowns work better when each action has its own rules</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Thu, 06 Aug 2026 06:46:06 +0000</pubDate>
      <link>https://dev.to/saqueib/laravel-cooldowns-work-better-when-each-action-has-its-own-rules-1jgi</link>
      <guid>https://dev.to/saqueib/laravel-cooldowns-work-better-when-each-action-has-its-own-rules-1jgi</guid>
      <description>&lt;p&gt;Rate limiting is a decent default, but it is a bad product policy for many real workflows. A user refreshing a dashboard, generating an AI summary, exporting a CSV, inviting teammates, and resending a notification are not equivalent actions. They have different cost profiles, different abuse patterns, and different UX expectations. Treating them all as "N requests per minute" is how you end up protecting the wrong thing while annoying the right users.&lt;/p&gt;

&lt;p&gt;In Laravel apps, &lt;strong&gt;per-action cooldowns&lt;/strong&gt; are often a better fit than one-size-fits-all rate limits. The model is simple: after a specific actor performs a specific expensive action, you block or delay just that action for a short period. The rest of the product keeps working. That gives you tighter abuse control without turning the entire app into a traffic cop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Rate Limits Get Too Blunt
&lt;/h2&gt;

&lt;p&gt;Laravel's built-in rate limiting tools are solid. If you need IP-based throttling, login protection, or API quotas, use them. The problem starts when you apply the same mechanism to workflows that are not really about request volume.&lt;/p&gt;

&lt;p&gt;An AI generation endpoint is not expensive because it gets called often. It is expensive because each call burns tokens, queue time, and maybe third-party API budget. An export button is not risky because it sees high RPS. It is risky because a single user can generate ten multi-megabyte exports back to back and bury your workers.&lt;/p&gt;

&lt;p&gt;That is why &lt;strong&gt;cooldowns map better to intent than throughput&lt;/strong&gt;. You are not saying, "this user may only make 20 requests per minute." You are saying, "this user may trigger this costly action once every 30 seconds." That is a very different policy, and usually the more correct one.&lt;/p&gt;

&lt;p&gt;A few common cases where cooldowns beat generic throttles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI content generation per workspace owner or user&lt;/li&gt;
&lt;li&gt;CSV, PDF, or ZIP exports&lt;/li&gt;
&lt;li&gt;Invite sending and reminder emails&lt;/li&gt;
&lt;li&gt;OTP resend or magic-link resend&lt;/li&gt;
&lt;li&gt;Notification blasts to teams or customers&lt;/li&gt;
&lt;li&gt;Billing-related recalculation jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key idea is narrow scope. Instead of globally slowing a user down, you cool down the exact thing that is expensive, spammy, or operationally risky.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model The Cooldown Around Ownership and Action
&lt;/h2&gt;

&lt;p&gt;The most useful cooldown key is usually &lt;strong&gt;owner + action&lt;/strong&gt;, not IP + route.&lt;/p&gt;

&lt;p&gt;Why owner? Because many expensive actions are tied to an account, team, tenant, or workspace budget. If one workspace generates ten AI reports in a minute, that cost lands on the workspace whether the requests came from one user, three users, or a job retried twice.&lt;/p&gt;

&lt;p&gt;Why action? Because not all expensive operations deserve the same waiting window. A resend-email button may need 60 seconds. AI generation may need 15 seconds. Export creation may need 2 minutes. A single global throttle cannot express that cleanly.&lt;/p&gt;

&lt;p&gt;A practical key format looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$cooldownKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cooldown:%s:%s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ownerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You then store the next allowed timestamp or use a cache lock/TTL to represent the wait window.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Minimal Laravel Service
&lt;/h3&gt;

&lt;p&gt;A small dedicated service keeps this logic out of controllers and makes the policy reusable.&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;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Support&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Contracts\Cache\Repository&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Carbon&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;ActionCooldowns&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;Cache&lt;/span&gt; &lt;span class="nv"&gt;$cache&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;activeFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$ownerId&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;$action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?Carbon&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$expiresAt&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="n"&gt;cache&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ownerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$expiresAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createFromTimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expiresAt&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;$time&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isFuture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$time&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;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;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$ownerId&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;$action&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;$seconds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Carbon&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$expiresAt&lt;/span&gt; &lt;span class="o"&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;addSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$seconds&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;cache&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ownerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nv"&gt;$expiresAt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$expiresAt&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;$expiresAt&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;enforce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$ownerId&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;$action&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$until&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;activeFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ownerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&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;'Action temporarily unavailable.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'retry_after_seconds'&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;diffInSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$until&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'retry_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$until&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toIso8601String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;429&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;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$ownerId&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;$action&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="s2"&gt;"cooldown:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ownerId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&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 intentionally boring. Boring is good here. You want a single place where cooldown policy is obvious, testable, and easy to extend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Cache TTL Is Usually Enough
&lt;/h3&gt;

&lt;p&gt;For cooldowns, Redis or your cache store is usually the right layer. You do not need a database table unless you need analytics, audit history, or operator visibility.&lt;/p&gt;

&lt;p&gt;A cache-backed cooldown has the right properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cheap reads and writes&lt;/li&gt;
&lt;li&gt;natural expiry&lt;/li&gt;
&lt;li&gt;shared across app servers&lt;/li&gt;
&lt;li&gt;good enough precision for product enforcement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are running multiple web nodes or queue workers, &lt;strong&gt;do not&lt;/strong&gt; keep cooldown state in memory. Use a shared backend such as Redis. Otherwise your policy becomes node-dependent, which is another way of saying broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Cooldowns At The Workflow Boundary
&lt;/h2&gt;

&lt;p&gt;The right place to enforce a cooldown is usually the boundary where cost or side effects begin. Not every controller deserves this. The actions that do are the ones that kick off work, send something external, or consume a priced dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: AI Generation
&lt;/h3&gt;

&lt;p&gt;This is the classic case. If a workspace can trigger a generation every few seconds, users can accidentally hammer your token budget just by clicking twice.&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;GenerateSummaryRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ActionCooldowns&lt;/span&gt; &lt;span class="nv"&gt;$cooldowns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;currentWorkspace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$cooldowns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enforce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$workspace&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;'ai-summary-generate'&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;GenerateSummaryJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;workspaceId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$workspace&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="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'prompt'&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;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nv"&gt;$cooldowns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$workspace&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;'ai-summary-generate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&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;'Generation queued.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;202&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;That policy is much sharper than &lt;code&gt;throttle:10,1&lt;/code&gt; on the route. It protects the exact action that costs money while allowing the user to keep navigating, editing, or fetching other data.&lt;/p&gt;

&lt;p&gt;A useful refinement is dynamic cooldowns. If the model is expensive or the prompt size is large, increase the cooldown. If the workspace is on a higher plan, shorten it. Cooldowns are product policy, so they should reflect product reality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Export Generation
&lt;/h3&gt;

&lt;p&gt;Exports tend to create operational spikes, especially when users click again because the UI did not make progress obvious.&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;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ActionCooldowns&lt;/span&gt; &lt;span class="nv"&gt;$cooldowns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$cooldowns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enforce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$account&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;'orders-export'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;ExportOrders&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;$account&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="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="nv"&gt;$cooldowns&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$account&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;'orders-export'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&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;'Export started. We will notify you when it is ready.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;202&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 does two things a global rate limit does not.&lt;/p&gt;

&lt;p&gt;First, it stops repeated export launches even if the user is otherwise behaving normally. Second, it creates a predictable product contract: one export every two minutes for this account.&lt;/p&gt;

&lt;p&gt;That is easier to explain, easier to support, and easier to reason about than a generic route throttle that may or may not trigger depending on unrelated requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UX Matters More Than The Backend Trick
&lt;/h2&gt;

&lt;p&gt;A cooldown without a clear UI is just a bug with a timer. If the user clicks a button and gets a 429 with no context, they will keep clicking, retry from another tab, or assume your app is broken.&lt;/p&gt;

&lt;p&gt;You need to surface the policy clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Return Structured Retry Data
&lt;/h3&gt;

&lt;p&gt;Do not just return "Too many requests." Return when the user can try again.&lt;/p&gt;

&lt;p&gt;A good response includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a stable error message&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retry_after_seconds&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;an absolute &lt;code&gt;retry_at&lt;/code&gt; timestamp&lt;/li&gt;
&lt;li&gt;optionally the action name for the client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives the frontend enough data to disable the button, start a countdown, and avoid blind retries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show Cooldowns In The Interface
&lt;/h3&gt;

&lt;p&gt;If a user triggers an action with a cooldown, the UI should reflect it immediately. Disable the action, show a countdown, and explain why. For AI generation, say it is preventing duplicate runs. For exports, say one export is already in progress or recently started.&lt;/p&gt;

&lt;p&gt;This is not decoration. It reduces duplicate work and support noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pair Cooldowns With Idempotency When Needed
&lt;/h3&gt;

&lt;p&gt;Cooldowns stop repeated triggers across a time window. They do not solve duplicate submissions caused by network retries or double form posts at the same instant. For that, use idempotency keys or job deduplication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cooldowns and idempotency solve different failures.&lt;/strong&gt; If the workflow is expensive enough, you usually want both.&lt;/p&gt;

&lt;p&gt;A good decision rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;idempotency&lt;/strong&gt; to prevent accidental duplicates of the same intent.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;cooldowns&lt;/strong&gt; to limit how often a costly intent may be started.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;rate limits&lt;/strong&gt; for broad traffic control, abuse protection, and API fairness.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failure Modes To Avoid
&lt;/h2&gt;

&lt;p&gt;Most bad cooldown implementations are not conceptually wrong. They are just attached to the wrong identity, stored in the wrong place, or tuned with no product thinking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cooldown By IP
&lt;/h3&gt;

&lt;p&gt;This is usually the wrong key for authenticated product workflows. Shared office IPs, VPNs, mobile networks, and browser privacy features make IP-based cooldowns noisy and unfair. If the action belongs to an account or workspace, key it that way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cooldown That Starts Too Early
&lt;/h3&gt;

&lt;p&gt;If you start the cooldown before basic validation or authorization, you can punish harmless user mistakes. Start it when the system actually accepts the action and begins meaningful work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cooldown That Starts Too Late
&lt;/h3&gt;

&lt;p&gt;If you only set the cooldown after a long-running job completes, repeated clicks can queue duplicates before the window exists. For queued workflows, start the cooldown at dispatch time or reserve the action with an atomic lock first.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Atomic Guard Around First Trigger
&lt;/h3&gt;

&lt;p&gt;If two requests arrive at nearly the same moment, both can pass the "not on cooldown" check before one writes the TTL. In high-contention actions, use Redis-backed atomic primitives such as locks or &lt;code&gt;Cache::add()&lt;/code&gt; semantics instead of a naive read-then-write sequence.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$acquired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cooldown:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ownerId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&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;addSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&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;addSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$seconds&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$acquired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Please wait before retrying this action.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;429&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;That closes the race for the first write in a simple way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inventing Cooldowns For Everything
&lt;/h3&gt;

&lt;p&gt;This is where teams go wrong after discovering the pattern. Not every action needs a cooldown. If the action is cheap, reversible, and already protected by validation, adding a wait window is just friction.&lt;/p&gt;

&lt;p&gt;Use cooldowns for actions that are at least one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expensive&lt;/li&gt;
&lt;li&gt;spam-prone&lt;/li&gt;
&lt;li&gt;operationally heavy&lt;/li&gt;
&lt;li&gt;externally visible&lt;/li&gt;
&lt;li&gt;confusing when triggered multiple times&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an action does not hit one of those, do not force it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Default For Real Laravel Products
&lt;/h2&gt;

&lt;p&gt;If your app has AI features, exports, invites, notifications, or any workflow with uneven cost, stop reaching for one generic throttle first. &lt;strong&gt;Per-action cooldowns give you a more honest policy surface.&lt;/strong&gt; They let you protect expensive paths tightly while leaving the rest of the product responsive.&lt;/p&gt;

&lt;p&gt;Laravel makes this easy because the underlying pieces are already there: cache, Redis, jobs, policies, and clean service abstractions. The trick is not technical difficulty. The trick is choosing the right control for the job.&lt;/p&gt;

&lt;p&gt;My recommendation is simple: keep Laravel rate limiting for broad API and auth protection, then add owner-scoped action cooldowns for workflows that cost real money or create real operational drag. When the action is expensive, user-facing, and easy to spam, a cooldown is usually the cleaner answer.&lt;/p&gt;

&lt;p&gt;If your current policy reads like "5 requests per minute" for a costly button, you probably have the wrong abstraction. Replace it with a rule the product can actually defend: &lt;strong&gt;who can do what, and how often, without harming cost, systems, or UX.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/per-action-cooldowns-laravel-rate-limits/" rel="noopener noreferrer"&gt;https://qcode.in/per-action-cooldowns-laravel-rate-limits/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>redis</category>
      <category>backend</category>
      <category>ratelimit</category>
    </item>
    <item>
      <title>Laravel image jobs need idempotency before they need new APIs</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Sat, 01 Aug 2026 05:24:34 +0000</pubDate>
      <link>https://dev.to/saqueib/laravel-image-jobs-need-idempotency-before-they-need-new-apis-18po</link>
      <guid>https://dev.to/saqueib/laravel-image-jobs-need-idempotency-before-they-need-new-apis-18po</guid>
      <description>&lt;p&gt;Laravel getting first-party image processing is useful, but it also creates the wrong instinct in a lot of teams. They start thinking the hard part is finally solved because the resize and transform layer is now closer to the framework.&lt;/p&gt;

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

&lt;p&gt;If your app handles uploads through queues, the dangerous part is still &lt;strong&gt;retries, partial completion, duplicate writes, and stale state&lt;/strong&gt;. A cleaner API for cropping or encoding does not fix a worker running twice, a timeout after two variants, or a replace flow that leaves old files live behind the CDN.&lt;/p&gt;

&lt;p&gt;My recommendation is blunt: &lt;strong&gt;treat Laravel image jobs as idempotent workflow code first and image manipulation code second&lt;/strong&gt;. If you get that order wrong, the pipeline will look fine in demos and slowly become operational debt in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Bug Is Usually Workflow Drift
&lt;/h2&gt;

&lt;p&gt;Most broken image systems are not broken because the JPEG library failed. They are broken because the surrounding workflow had no durable memory.&lt;/p&gt;

&lt;p&gt;A typical naive implementation looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Accept upload&lt;/li&gt;
&lt;li&gt;Save original with a random name&lt;/li&gt;
&lt;li&gt;Dispatch a queued job&lt;/li&gt;
&lt;li&gt;Generate a few variants&lt;/li&gt;
&lt;li&gt;Update the database at the end&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds reasonable until reality arrives.&lt;/p&gt;

&lt;p&gt;What happens if the worker dies after writing the thumbnail but before writing the hero image? What happens if the queue retries the job automatically? What happens if the user re-uploads a replacement while the first job is still running? What happens if S3 accepts one object write but the DB transaction rolls back? What happens if Horizon runs the same job again after a timeout even though some files are already present?&lt;/p&gt;

&lt;p&gt;Those are not weird edge cases. They are normal production behavior once your queue, storage, and HTTP boundary stop behaving like one process.&lt;/p&gt;

&lt;p&gt;Laravel's queue docs already push you toward this mental model: jobs may be retried, may time out, and may be processed asynchronously by separate workers. That is exactly why image work must be retry-safe in the first place: &lt;a href="https://laravel.com/docs/13.x/queues" rel="noopener noreferrer"&gt;https://laravel.com/docs/13.x/queues&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If your image pipeline assumes every job runs exactly once from start to finish, you do not have a resilient pipeline. You have a best-case script.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Failure Modes Worth Designing For
&lt;/h3&gt;

&lt;p&gt;The recurring failure modes are boring, which is exactly why teams under-design them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate original files for the same logical image&lt;/li&gt;
&lt;li&gt;half-generated variants with no reliable way to resume&lt;/li&gt;
&lt;li&gt;a DB row marked &lt;code&gt;processed&lt;/code&gt; while one or more files are missing&lt;/li&gt;
&lt;li&gt;an older job overwriting paths after a newer upload already replaced the image&lt;/li&gt;
&lt;li&gt;retries generating the same derivatives repeatedly and inflating storage cost&lt;/li&gt;
&lt;li&gt;cleanup code deleting active files because it cannot distinguish versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread is simple: &lt;strong&gt;the system cannot tell what work is intended, what work is complete, and what work has been superseded&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is an idempotency problem, not an imaging problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With A Durable Image Record, Not With &lt;code&gt;Storage::put()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The first design move should be a database record that represents the lifecycle of a logical image. Do that before you worry about which driver or image library you prefer.&lt;/p&gt;

&lt;p&gt;You want one place that can answer these questions at any moment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what is the canonical original for this image?&lt;/li&gt;
&lt;li&gt;which processing version is current?&lt;/li&gt;
&lt;li&gt;what status is the workflow in right now?&lt;/li&gt;
&lt;li&gt;which variants are already durable?&lt;/li&gt;
&lt;li&gt;was this image replaced, failed, or completed?&lt;/li&gt;
&lt;li&gt;do two uploads contain the same original bytes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal schema can carry most of that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&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;'media_images'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;uuid&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;morphs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'imageable'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'disk'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&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;index&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'version'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&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;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'original_path'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'original_extension'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'original_checksum'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&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;index&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedBigInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'original_bytes'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mime_type'&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="nv"&gt;$table&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;'variants'&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;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&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;'meta'&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;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'processing_started_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'processed_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'failed_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'failure_reason'&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;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'superseded_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&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;That table is doing real work.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;status&lt;/code&gt; field gives the queue something explicit to reconcile against. The &lt;code&gt;version&lt;/code&gt; field gives replacements a clean upgrade path. The &lt;code&gt;original_checksum&lt;/code&gt; gives you content identity instead of guessing from filenames. The &lt;code&gt;variants&lt;/code&gt; JSON gives you durable progress tracking without having to scan storage on every decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Status Columns Matter More Than People Admit
&lt;/h3&gt;

&lt;p&gt;A lot of teams try to infer state from file existence. If &lt;code&gt;thumb.webp&lt;/code&gt; exists, they treat the image as processed. That is fragile.&lt;/p&gt;

&lt;p&gt;File existence alone cannot tell you whether:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the current version is the one on disk&lt;/li&gt;
&lt;li&gt;all required variants are present&lt;/li&gt;
&lt;li&gt;the database metadata matches the actual artifacts&lt;/li&gt;
&lt;li&gt;the image was replaced and the old path is now stale&lt;/li&gt;
&lt;li&gt;the workflow failed after partial success&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use explicit workflow states instead. Keep them small and operationally meaningful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pending&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;processing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;processed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;superseded&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is enough for most Laravel apps. Resist the urge to invent a state machine with 15 statuses unless you genuinely need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deterministic Paths Are The Backbone Of Retry Safety
&lt;/h2&gt;

&lt;p&gt;Randomized filenames are fine for raw uploads at the edge. They are bad as the long-term identity of a processing workflow.&lt;/p&gt;

&lt;p&gt;If every retry produces a different target path, you cannot safely answer basic questions like "did we already write this variant?" or "which files belong to version 2 of this image?" You may avoid collisions, but you create a bigger mess: hidden duplicates, expensive cleanup, and no stable namespace.&lt;/p&gt;

&lt;p&gt;A better rule is this: &lt;strong&gt;each logical image should own a deterministic directory, and each version should own deterministic child paths&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For 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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImagePaths&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;original&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;$imageId&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;$version&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;$extension&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="s2"&gt;"images/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$imageId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/original.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$extension&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&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;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;variant&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;$imageId&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;$version&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;$name&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;$extension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'webp'&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="s2"&gt;"images/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$imageId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$extension&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&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;That path scheme is boring, which is exactly what you want.&lt;/p&gt;

&lt;p&gt;It gives you stable targets across retries, clean version isolation across replacements, and predictable cleanup scopes. It also plays nicely with Laravel's filesystem abstraction because the framework can switch disks without changing your naming contract: &lt;a href="https://laravel.com/docs/13.x/filesystem" rel="noopener noreferrer"&gt;https://laravel.com/docs/13.x/filesystem&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Versioned Paths Beat Overwrites
&lt;/h3&gt;

&lt;p&gt;Many teams try to overwrite &lt;code&gt;original.jpg&lt;/code&gt; and regenerate the same variant paths in place when an image is replaced. That looks simpler until concurrency shows up.&lt;/p&gt;

&lt;p&gt;Imagine this timeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User uploads image A&lt;/li&gt;
&lt;li&gt;Job A starts generating variants&lt;/li&gt;
&lt;li&gt;User replaces it with image B&lt;/li&gt;
&lt;li&gt;Job B starts&lt;/li&gt;
&lt;li&gt;Job A finishes late and writes over shared paths&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now you have stale data winning because the path contract allowed two generations to compete for the same filenames.&lt;/p&gt;

&lt;p&gt;Versioned paths stop that class of bug. Job A writes only under &lt;code&gt;v1&lt;/code&gt;. Job B writes only under &lt;code&gt;v2&lt;/code&gt;. The database decides which version is current. Cleanup can remove &lt;code&gt;v1&lt;/code&gt; later, but only after &lt;code&gt;v2&lt;/code&gt; is fully durable.&lt;/p&gt;

&lt;p&gt;That is a much cleaner system boundary than trying to make timing guarantees you do not actually control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checksums Turn Upload Chaos Into Something You Can Reason About
&lt;/h2&gt;

&lt;p&gt;The checksum is the underrated field in this whole pipeline.&lt;/p&gt;

&lt;p&gt;Without it, repeated uploads are guesswork. With it, you can make concrete decisions.&lt;/p&gt;

&lt;p&gt;If the user retries the same request, or your frontend double-submits, or an API client replays an upload after a network wobble, the checksum lets you tell whether this is genuinely new content or the same file arriving twice.&lt;/p&gt;

&lt;p&gt;That creates practical rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same image record + same checksum: do not regenerate everything&lt;/li&gt;
&lt;li&gt;same image record + different checksum: increment version and reprocess&lt;/li&gt;
&lt;li&gt;different record + same checksum: maybe deduplicate later, but only if the added complexity is worth it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per-record idempotency is usually enough. Global deduplication across the whole app sounds clever, but it adds reference counting, ownership questions, and cleanup complexity fast. Most teams should earn that complexity only after local idempotency is solid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compute The Checksum At Ingress
&lt;/h3&gt;

&lt;p&gt;You want the checksum as early as possible, ideally when the original file is first accepted:&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;$stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$uploadedFile&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRealPath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'rb'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$checksum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$uploadedFile&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRealPath&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nv"&gt;$extension&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="nv"&gt;$uploadedFile&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getClientOriginalExtension&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'bin'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaImage&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;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&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="s1"&gt;'imageable_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&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="s1"&gt;'imageable_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'disk'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'s3'&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;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'original_extension'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$extension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'original_checksum'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$checksum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'original_bytes'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$uploadedFile&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'mime_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$uploadedFile&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMimeType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'original_path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ImagePaths&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;original&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$extension&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&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;'s3'&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="nv"&gt;$image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;original_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$stream&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 also where you want to make a judgment call about request-level idempotency. If your API surface can receive the same upload twice from a flaky client, it is often worth pairing the checksum with an application-level idempotency key so you can return the existing image record instead of creating a new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Job Should Reconcile Progress, Not Rebuild Blindly
&lt;/h2&gt;

&lt;p&gt;The core mistake in most image jobs is that they act like a one-shot script. They should act like a reconciler.&lt;/p&gt;

&lt;p&gt;A reconciler asks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what state should exist?&lt;/li&gt;
&lt;li&gt;what state already exists?&lt;/li&gt;
&lt;li&gt;what is missing?&lt;/li&gt;
&lt;li&gt;what has become obsolete?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That mental model is better than "run all transforms and hope it finishes" because retries become safe by construction.&lt;/p&gt;

&lt;p&gt;Here is the shape I prefer:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessImageVariants&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="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&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;string&lt;/span&gt; &lt;span class="nv"&gt;$imageId&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;$version&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;handle&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;$image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaImage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findOrFail&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;imageId&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;$image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&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="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$image&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="s1"&gt;'superseded'&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="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;$image&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="s1"&gt;'processed'&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="nf"&gt;allRequiredVariantsExist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="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="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forceFill&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;'processing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'processing_started_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;processing_started_at&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'failed_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'failure_reason'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;variants&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;variantDefinitions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$definition&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="nf"&gt;variantAlreadyDurable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$binary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImageVariantBuilder&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$definition&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'width'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$definition&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'height'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$definition&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'fit'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ImagePaths&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="o"&gt;-&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="nv"&gt;$image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'webp'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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="nv"&gt;$image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;disk&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="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$binary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nv"&gt;$written&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'width'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$definition&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'width'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'height'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$definition&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'height'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'format'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'webp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;];&lt;/span&gt;

            &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forceFill&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'variants'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$written&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;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;forceFill&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;'processed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'processed_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="s1"&gt;'variants'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$written&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;save&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;There are three important ideas here.&lt;/p&gt;

&lt;p&gt;First, the job exits early if it is no longer the current version. That prevents stale work from winning late.&lt;/p&gt;

&lt;p&gt;Second, the job persists progress after each successful variant. That means a crash after two variants is annoying, not catastrophic.&lt;/p&gt;

&lt;p&gt;Third, completion is earned only after the required set is actually present.&lt;/p&gt;

&lt;h3&gt;
  
  
  Incremental Persistence Beats Fake Atomicity
&lt;/h3&gt;

&lt;p&gt;You cannot make storage writes and database writes one perfect transaction across systems. Pretending otherwise just hides the truth.&lt;/p&gt;

&lt;p&gt;The pragmatic answer is incremental honesty:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;mark &lt;code&gt;processing&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;write one artifact&lt;/li&gt;
&lt;li&gt;persist that artifact's metadata&lt;/li&gt;
&lt;li&gt;continue&lt;/li&gt;
&lt;li&gt;mark &lt;code&gt;processed&lt;/code&gt; only when the full required set exists&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That pattern gives retries something real to work with. It also makes operator debugging much easier because the record reflects partial progress instead of collapsing everything into a binary success/failure fantasy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency And Replacement Flows Need Explicit Rules
&lt;/h2&gt;

&lt;p&gt;Image systems usually get messy when the same logical image can be edited, replaced, or reprocessed while workers are still active.&lt;/p&gt;

&lt;p&gt;Do not solve that with hope. Solve it with rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 1: Jobs Must Carry Version Context
&lt;/h3&gt;

&lt;p&gt;Never dispatch a generic "process this image" job without the version it is supposed to process. Pass both image ID and version.&lt;/p&gt;

&lt;p&gt;That way, when the worker wakes up, it can immediately verify whether it still owns relevant work. If the DB row has already advanced to a newer version, the old job should do nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 2: Replacements Should Supersede, Not Mutate In Place
&lt;/h3&gt;

&lt;p&gt;When the original changes, do not quietly rewrite the existing artifacts. Increment &lt;code&gt;version&lt;/code&gt;, write the new original under the new namespace, reset status to &lt;code&gt;pending&lt;/code&gt;, and dispatch a fresh job.&lt;/p&gt;

&lt;p&gt;The old version can remain on disk for a short time. That is fine. Temporary duplication is much safer than cross-version corruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3: Cleanup Should Be Separate From Processing
&lt;/h3&gt;

&lt;p&gt;Do not mix destructive cleanup into the happy-path processing job unless you absolutely need to. A failed cleanup should not poison image generation.&lt;/p&gt;

&lt;p&gt;Use a later job that removes obsolete versions only after the new one is fully &lt;code&gt;processed&lt;/code&gt; and any publish/CDN rules are satisfied.&lt;/p&gt;

&lt;p&gt;That separation keeps the hot path smaller and prevents one category of failure from cascading into another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage Writes Need To Be Retry-Safe Too
&lt;/h2&gt;

&lt;p&gt;A surprising number of pipelines are "idempotent" in theory but still perform wasteful or dangerous writes in practice.&lt;/p&gt;

&lt;p&gt;The simplest rule is this: &lt;strong&gt;before writing a variant, know the exact target path and know whether an existing durable file already satisfies the contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That often means checking both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the DB metadata says the variant exists&lt;/li&gt;
&lt;li&gt;the storage disk confirms the path exists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If both are true, skip the write. If the DB says it exists but storage disagrees, repair it. If storage says it exists but the DB row never recorded it, verify it belongs to the current version before trusting it.&lt;/p&gt;

&lt;p&gt;That sounds like extra bookkeeping because it is. That bookkeeping is what turns retries into recovery instead of duplication.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 And Local Disk Fail Differently
&lt;/h3&gt;

&lt;p&gt;Laravel makes local disk and S3 look similar from the app code, which is a good abstraction. But operationally, the tradeoffs are still different.&lt;/p&gt;

&lt;p&gt;Local disk gives you lower latency and simple existence checks but ties durability to host topology. S3 gives you better durability and easier horizontal scale but makes object writes, verification, and cleanup more distributed in feel.&lt;/p&gt;

&lt;p&gt;The idempotency rules do not change across disks. What changes is how much you trust path existence, how expensive repeated writes are, and how carefully you want to stage cleanup.&lt;/p&gt;

&lt;p&gt;That is why designing around deterministic paths and explicit workflow state matters so much. It survives infrastructure changes better than path conventions invented ad hoc in controllers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Ship In A Real Laravel App
&lt;/h2&gt;

&lt;p&gt;If I were implementing image jobs in a production Laravel codebase today, the baseline would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one DB row per logical image&lt;/li&gt;
&lt;li&gt;explicit &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;version&lt;/code&gt; columns&lt;/li&gt;
&lt;li&gt;checksum captured at upload time&lt;/li&gt;
&lt;li&gt;deterministic original and variant paths&lt;/li&gt;
&lt;li&gt;queued jobs that receive image ID plus version&lt;/li&gt;
&lt;li&gt;per-variant progress persistence&lt;/li&gt;
&lt;li&gt;completion only after required variants are verified&lt;/li&gt;
&lt;li&gt;replacement flows that create new versions instead of overwriting paths&lt;/li&gt;
&lt;li&gt;cleanup handled asynchronously after successful supersession&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not over-engineering. That is the minimum structure that keeps image work understandable once retries and replacements show up.&lt;/p&gt;

&lt;p&gt;New first-party image APIs are a good addition. They should reduce glue code, make transforms more Laravel-native, and clean up the manipulation layer. Use them.&lt;/p&gt;

&lt;p&gt;Just do not confuse a better transform API with a reliable image pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical Decision Rule
&lt;/h3&gt;

&lt;p&gt;If an image job can run twice, then it must be idempotent. That means stable identity, deterministic paths, explicit workflow state, and retry-safe writes. Everything else is secondary.&lt;/p&gt;

&lt;p&gt;In other words: &lt;strong&gt;the best Laravel image pipeline is not the one with the nicest resize syntax. It is the one that can crash halfway through, run again, and still converge on the same correct result.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/laravel-image-jobs-need-idempotency-more-than-new-apis/" rel="noopener noreferrer"&gt;https://qcode.in/laravel-image-jobs-need-idempotency-more-than-new-apis/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>queues</category>
      <category>s3</category>
    </item>
    <item>
      <title>Using HTTP QUERY in Laravel Without Making Your API Harder to Maintain</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Thu, 30 Jul 2026 04:20:25 +0000</pubDate>
      <link>https://dev.to/saqueib/using-http-query-in-laravel-without-making-your-api-harder-to-maintain-19kf</link>
      <guid>https://dev.to/saqueib/using-http-query-in-laravel-without-making-your-api-harder-to-maintain-19kf</guid>
      <description>&lt;p&gt;The short version is this: &lt;strong&gt;&lt;code&gt;GET&lt;/code&gt; should remain your default search method in Laravel, &lt;code&gt;POST&lt;/code&gt; is still the pragmatic fallback for complex filters, and &lt;code&gt;QUERY&lt;/code&gt; is only worth it when you care enough about HTTP semantics to also absorb the infrastructure cost.&lt;/strong&gt; That is the real trade.&lt;/p&gt;

&lt;p&gt;The new &lt;strong&gt;HTTP &lt;code&gt;QUERY&lt;/code&gt;&lt;/strong&gt; method is no longer just an idea floating around in standards discussions. It became an RFC in &lt;strong&gt;June 2026&lt;/strong&gt; as &lt;a href="https://datatracker.ietf.org/doc/rfc10008/" rel="noopener noreferrer"&gt;RFC 10008&lt;/a&gt;. Its pitch is clean: let clients send a &lt;strong&gt;safe, idempotent request with a body&lt;/strong&gt;. In other words, keep the semantics of retrieval while avoiding the ugliness of packing large search payloads into query strings or pretending a safe read is a &lt;code&gt;POST&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That sounds perfect for search APIs. Laravel teams, especially the ones building admin panels, reporting endpoints, AI-assisted filtering, or multi-criteria dashboards, will look at it and think: finally, a method that matches the actual intent.&lt;/p&gt;

&lt;p&gt;The catch is that protocol correctness is only half the job. The other half is everything around it: proxies, clients, caches, API gateways, analytics tools, team familiarity, browser behavior, and fallback design. &lt;code&gt;QUERY&lt;/code&gt; can absolutely make an API cleaner. It can also become one of those technically elegant choices that quietly increases maintenance drag for the next two years.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;QUERY&lt;/code&gt; Fixes That &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;POST&lt;/code&gt; Do Not
&lt;/h2&gt;

&lt;p&gt;The value of &lt;code&gt;QUERY&lt;/code&gt; is easiest to see when your search endpoint has outgrown a nice URL.&lt;/p&gt;

&lt;p&gt;Simple filtering belongs on &lt;code&gt;GET&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /orders?status=paid&amp;amp;sort=-created_at&amp;amp;page=2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is still the best option when the filter is small, linkable, and cache-friendly. Laravel, browsers, CDNs, observability tools, and every API client on earth understand it.&lt;/p&gt;

&lt;p&gt;The problem starts when search payloads get richer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nested filters&lt;/li&gt;
&lt;li&gt;grouped conditions&lt;/li&gt;
&lt;li&gt;long lists of IDs&lt;/li&gt;
&lt;li&gt;full-text rules with weights&lt;/li&gt;
&lt;li&gt;date windows and facets&lt;/li&gt;
&lt;li&gt;export-style report queries&lt;/li&gt;
&lt;li&gt;AI-generated filter payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, teams usually jump to &lt;code&gt;POST /orders/search&lt;/code&gt; with JSON. It works, but it muddies the semantics. You are making a read-only operation look like a state-changing write. RFC 10008 exists precisely for that gap. It defines &lt;code&gt;QUERY&lt;/code&gt; as &lt;strong&gt;safe and idempotent&lt;/strong&gt;, similar to &lt;code&gt;GET&lt;/code&gt;, while still allowing request content in the body. The RFC explicitly describes it as the bridge between URI-based &lt;code&gt;GET&lt;/code&gt; queries and body-based &lt;code&gt;POST&lt;/code&gt; queries.&lt;/p&gt;

&lt;p&gt;That semantic clarity is not academic. It affects retry logic, caching assumptions, API readability, and how future maintainers reason about your endpoint. A &lt;code&gt;POST&lt;/code&gt; search route always forces a mental footnote: "yes, this is actually a read." A &lt;code&gt;QUERY&lt;/code&gt; route does not.&lt;/p&gt;

&lt;p&gt;There is another nice detail in the RFC: support can be discovered with the standard &lt;code&gt;Allow&lt;/code&gt; header and with the new &lt;code&gt;Accept-Query&lt;/code&gt; response header, which can advertise supported query body media types. In spec terms, this is cleaner than the usual undocumented convention of "POST here with JSON and trust us."&lt;/p&gt;

&lt;p&gt;So if you are comparing the methods purely on semantic accuracy, the ranking is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/strong&gt; wins for small, URL-friendly searches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;QUERY&lt;/code&gt;&lt;/strong&gt; wins for complex, body-based searches that are still safe reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/strong&gt; is the compatibility workhorse, not the cleanest model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the good part. The harder question is whether your actual stack can live with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where &lt;code&gt;QUERY&lt;/code&gt; Really Helps in a Laravel Codebase
&lt;/h2&gt;

&lt;p&gt;If you are going to adopt &lt;code&gt;QUERY&lt;/code&gt;, do it for endpoints where the semantic gain is real, not cosmetic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Fit: Complex Safe Search
&lt;/h3&gt;

&lt;p&gt;The best case is a search or reporting endpoint whose payload is too large or structured for a query string, but which still has no side effects.&lt;/p&gt;

&lt;p&gt;Think of an internal analytics screen with nested filter groups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"paid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refunded"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IN"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"date_range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-27"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"revenue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"desc"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"per_page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This payload is awkward in &lt;code&gt;GET&lt;/code&gt;, but it is clearly not a write. &lt;code&gt;QUERY&lt;/code&gt; matches the intent better than &lt;code&gt;POST&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better Fit for Shared Search Engines
&lt;/h3&gt;

&lt;p&gt;If your Laravel app exposes a search abstraction used by multiple clients, &lt;code&gt;QUERY&lt;/code&gt; can also make the contract clearer. Mobile apps, internal tools, backend services, and AI agents all understand a resource that is being queried, not mutated.&lt;/p&gt;

&lt;p&gt;That matters when your API surface starts to sprawl. Naming every safe search route &lt;code&gt;.../search&lt;/code&gt; behind &lt;code&gt;POST&lt;/code&gt; works, but it slowly trains your codebase to treat reads as writes whenever the filter becomes inconvenient.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Cleaner Service Boundary
&lt;/h3&gt;

&lt;p&gt;The good Laravel design here is not "put search logic in the route and celebrate modern HTTP." The good design is to normalize all search inputs into a single query object and let transport be an edge concern.&lt;/p&gt;

&lt;p&gt;For 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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderSearchData&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;array&lt;/span&gt; &lt;span class="nv"&gt;$filters&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="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$sort&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="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$perPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&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;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fromRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;self&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'QUERY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&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;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filters'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
            &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
            &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;data_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'date_range.from'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;data_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'date_range.to'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'page'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;perPage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'per_page'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;50&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="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;That pattern matters because if you adopt &lt;code&gt;QUERY&lt;/code&gt;, you should almost certainly also keep a compatibility path for &lt;code&gt;GET&lt;/code&gt; or &lt;code&gt;POST&lt;/code&gt;. The application core should not care which transport brought the search payload in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;QUERY&lt;/code&gt; Can Still Become a Maintenance Trap
&lt;/h2&gt;

&lt;p&gt;This is the part people skip when they get excited about standards.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;QUERY&lt;/code&gt; is a better semantic fit than &lt;code&gt;POST&lt;/code&gt; for body-based reads. That does &lt;strong&gt;not&lt;/strong&gt; mean it is a better operational choice for every Laravel team in 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Laravel Is Friendly, the Rest of the Stack May Not Be
&lt;/h3&gt;

&lt;p&gt;Laravel’s request object is flexible. The docs explicitly show that &lt;code&gt;Request::method()&lt;/code&gt; returns the incoming verb and &lt;code&gt;isMethod()&lt;/code&gt; can test it, which means the framework can inspect whatever method reaches it. Laravel routing docs also show first-class helpers only for the common verbs plus &lt;code&gt;match()&lt;/code&gt; and &lt;code&gt;any()&lt;/code&gt;. That gap is telling.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;QUERY&lt;/code&gt; is not a first-class Laravel happy path yet. You are stepping off the paved road.&lt;/p&gt;

&lt;p&gt;That means you have to think beyond framework code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Will your web server pass the method through untouched?&lt;/li&gt;
&lt;li&gt;Will your load balancer or API gateway allow it?&lt;/li&gt;
&lt;li&gt;Will your WAF rules treat it as suspicious by default?&lt;/li&gt;
&lt;li&gt;Will your request logs, dashboards, and APM tooling group it correctly?&lt;/li&gt;
&lt;li&gt;Will your SDKs, test tools, and generated clients preserve it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A method can be valid HTTP and still be awkward in real infrastructure for years.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser and Client Ergonomics Are Still Uneven
&lt;/h3&gt;

&lt;p&gt;This is where many elegant API ideas lose momentum. Your server may accept &lt;code&gt;QUERY&lt;/code&gt;, but your consumers are not just servers. They are browsers, frontend apps, mobile clients, Postman collections, CLI tools, generated SDKs, and internal scripts.&lt;/p&gt;

&lt;p&gt;Even when a client can technically send a custom method, that does not mean the surrounding tooling treats it as normal. Teams end up discovering soft failures instead of hard ones: middleware assumptions, CORS friction, analytics blind spots, auto-generated docs that misrender the method, or testing utilities that need manual overrides.&lt;/p&gt;

&lt;p&gt;A clean spec with awkward tooling adoption is exactly how maintenance traps get born.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching Is Better in Theory Than in Commodity Infrastructure
&lt;/h3&gt;

&lt;p&gt;RFC 10008 says &lt;code&gt;QUERY&lt;/code&gt; responses are cacheable. That is a real semantic advantage over the usual hand-wavy &lt;code&gt;POST&lt;/code&gt; search pattern. But the same RFC also points out that caching &lt;code&gt;QUERY&lt;/code&gt; is inherently more complex than &lt;code&gt;GET&lt;/code&gt;, because the cache key has to account for the request body.&lt;/p&gt;

&lt;p&gt;That is the key practical point. Commodity caches and CDNs are built around &lt;code&gt;GET&lt;/code&gt; muscle memory. Once your cache key depends on request content, you need much more confidence in the behavior of your intermediaries.&lt;/p&gt;

&lt;p&gt;So yes, &lt;code&gt;QUERY&lt;/code&gt; is semantically more cacheable than &lt;code&gt;POST&lt;/code&gt;. No, that does not mean your stack will suddenly deliver effortless query-body caching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability Gets Slightly Worse Before It Gets Better
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;GET&lt;/code&gt; requests are easy to inspect because the filters are in the URL. That is noisy, but operationally convenient. &lt;code&gt;POST&lt;/code&gt; requests are boring but familiar. &lt;code&gt;QUERY&lt;/code&gt; sits in an awkward middle state: safe like &lt;code&gt;GET&lt;/code&gt;, body-based like &lt;code&gt;POST&lt;/code&gt;, and uncommon enough that some tooling will not know what to do with it by default.&lt;/p&gt;

&lt;p&gt;If your team relies heavily on request logs, metrics tagging, or ops dashboards, uncommon verbs create extra work. Not impossible work. Just work you need to budget for before calling the design "cleaner."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Laravel Implementation Pattern That Makes Sense
&lt;/h2&gt;

&lt;p&gt;If you want to experiment with &lt;code&gt;QUERY&lt;/code&gt;, do it with a &lt;strong&gt;dual-path design&lt;/strong&gt;, not a purity play.&lt;/p&gt;

&lt;p&gt;The wrong rollout is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invent a &lt;code&gt;QUERY&lt;/code&gt; endpoint&lt;/li&gt;
&lt;li&gt;remove &lt;code&gt;POST&lt;/code&gt; search immediately&lt;/li&gt;
&lt;li&gt;assume infrastructure will cooperate&lt;/li&gt;
&lt;li&gt;discover breakage one proxy or one client at a time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The better rollout is to centralize search logic and expose multiple transports intentionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended Route Shape
&lt;/h3&gt;

&lt;p&gt;Keep your canonical search behavior in one action or service, then expose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET&lt;/code&gt; for simple filters&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST&lt;/code&gt; as the broad compatibility route&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;QUERY&lt;/code&gt; as the semantically correct route for capable clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\OrderSearchController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&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;'/orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;OrderSearchController&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="s1"&gt;'index'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/orders/search'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;OrderSearchController&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="s1"&gt;'search'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/orders/query'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;OrderSearchController&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="s1"&gt;'query'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderSearchController&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;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;OrderSearch&lt;/span&gt; &lt;span class="nv"&gt;$search&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderSearchData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$search&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="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;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;OrderSearch&lt;/span&gt; &lt;span class="nv"&gt;$search&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderSearchData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$search&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="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;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;OrderSearch&lt;/span&gt; &lt;span class="nv"&gt;$search&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;abort_unless&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;isMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'QUERY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderSearchData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$search&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Accept-Query'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&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 not pretty in a framework-purist sense, but it is honest. It acknowledges that Laravel does not yet hand you a polished &lt;code&gt;Route::query()&lt;/code&gt; abstraction and that compatibility still matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validation and Idempotency Still Matter
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;QUERY&lt;/code&gt; is safe and idempotent, your implementation needs to behave like it. That sounds obvious, but it is easy to accidentally violate.&lt;/p&gt;

&lt;p&gt;Do not let a search endpoint with &lt;code&gt;QUERY&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persist "last searched" state as a side effect&lt;/li&gt;
&lt;li&gt;write audit rows on every request unless you are comfortable treating them as benign side effects&lt;/li&gt;
&lt;li&gt;trigger exports, notifications, or queue jobs automatically&lt;/li&gt;
&lt;li&gt;warm expensive materialized views in a way that behaves like a write API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you label an endpoint as &lt;code&gt;QUERY&lt;/code&gt;, keep it operationally read-only. Otherwise you get the worst of both worlds: unfamiliar method plus broken semantics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;Accept-Query&lt;/code&gt; If You Are Serious
&lt;/h3&gt;

&lt;p&gt;If you adopt &lt;code&gt;QUERY&lt;/code&gt;, do not hide it as tribal knowledge. RFC 10008 added the &lt;code&gt;Accept-Query&lt;/code&gt; response header for a reason. If your endpoint supports JSON query bodies, advertise that.&lt;/p&gt;

&lt;p&gt;That makes the feature discoverable and gives your API contract a little more honesty than a random internal wiki page.&lt;/p&gt;

&lt;h2&gt;
  
  
  So Should Laravel Teams Use &lt;code&gt;QUERY&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Usually, &lt;strong&gt;not as the only search method&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the recommendation I would defend in a real code review.&lt;/p&gt;

&lt;p&gt;If your audience is broad, public, browser-heavy, or integration-heavy, &lt;code&gt;GET&lt;/code&gt; plus &lt;code&gt;POST&lt;/code&gt; is still the safest pairing. &lt;code&gt;GET&lt;/code&gt; covers the simple and URL-friendly cases. &lt;code&gt;POST&lt;/code&gt; covers complex body-based search without forcing the rest of your stack to learn a still-uncommon verb.&lt;/p&gt;

&lt;p&gt;If your environment is controlled, your clients are known, your infrastructure team can validate method pass-through, and you care enough about protocol semantics to maintain the edges properly, then &lt;code&gt;QUERY&lt;/code&gt; becomes worth considering. In that narrower setup, it is genuinely cleaner than pretending every complex read is a &lt;code&gt;POST&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the real comparison looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;GET&lt;/code&gt;&lt;/strong&gt; when the search is naturally representable in the URL and benefits from universal tooling support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;POST&lt;/code&gt;&lt;/strong&gt; when compatibility matters more than semantic purity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;QUERY&lt;/code&gt;&lt;/strong&gt; when the operation is truly a safe read, the payload belongs in the body, and your stack is mature enough to support a less-traveled method without surprises.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last condition is the whole story. &lt;code&gt;QUERY&lt;/code&gt; is not a gimmick, and it is not automatically a trap. It is a sharper tool that only pays off when the rest of the system is ready for it.&lt;/p&gt;

&lt;p&gt;If your team is still fighting basic API consistency, do not start here. If your team already cares about transport semantics, cache behavior, and multi-client search design, then &lt;code&gt;QUERY&lt;/code&gt; is finally a real option. Just adopt it like an infrastructure decision, not a syntax upgrade.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/http-query-laravel-cleaner-search-api-future-maintenance-trap/" rel="noopener noreferrer"&gt;https://qcode.in/http-query-laravel-cleaner-search-api-future-maintenance-trap/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>api</category>
      <category>http</category>
      <category>backend</category>
    </item>
    <item>
      <title>Claude Code on Bun: What Runtime Choices Actually Mean for Agentic Tools</title>
      <dc:creator>Saqueib Ansari</dc:creator>
      <pubDate>Tue, 28 Jul 2026 04:08:26 +0000</pubDate>
      <link>https://dev.to/saqueib/claude-code-on-bun-what-runtime-choices-actually-mean-for-agentic-tools-3bme</link>
      <guid>https://dev.to/saqueib/claude-code-on-bun-what-runtime-choices-actually-mean-for-agentic-tools-3bme</guid>
      <description>&lt;p&gt;Claude Code’s move through a &lt;strong&gt;Bun plus Rust&lt;/strong&gt; story is not interesting because it proves one runtime is universally better. It is interesting because it exposes what agentic developer tools actually optimize for once they stop being simple CLIs and start acting like local operators.&lt;/p&gt;

&lt;p&gt;A coding agent is a weird product. It has to boot fast enough to feel conversational, stream output without stalling, spawn real developer tools, survive broken local environments, package cleanly across platforms, and remain debuggable when users say, "it got stuck after editing three files and running tests." That is a very different runtime problem than building a web API.&lt;/p&gt;

&lt;p&gt;So the right takeaway is not "everyone should rewrite their tools on Bun". The right takeaway is that &lt;strong&gt;agent runtimes are chosen by friction, control, and failure behavior more than by raw benchmark throughput&lt;/strong&gt;. Claude Code’s direction just makes that reality easier to see.&lt;/p&gt;

&lt;p&gt;Anthropic’s public material now makes that connection fairly explicit. Bun announced in December 2025 that Anthropic was betting on it as infrastructure for Claude Code and future AI coding tools, and Anthropic has separately written about Claude Code’s sandboxing model and agent SDK direction. Those are not isolated product notes. Together they point to a broader architecture bias: agent tools want a tighter runtime surface, not a looser one. See &lt;a href="https://www.anthropic.com/news/anthropic-acquires-bun-as-claude-code-reaches-usd1b-milestone" rel="noopener noreferrer"&gt;Anthropic’s acquisition note&lt;/a&gt;, &lt;a href="https://bun.sh/blog/bun-joins-anthropic" rel="noopener noreferrer"&gt;Bun’s announcement&lt;/a&gt;, and &lt;a href="https://www.anthropic.com/engineering/claude-code-sandboxing" rel="noopener noreferrer"&gt;Anthropic’s sandboxing write-up&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic Tools Punish the Wrong Runtime Fast
&lt;/h2&gt;

&lt;p&gt;Developers still talk about runtimes as if the main question is request throughput or ecosystem size. For a coding agent, those are secondary. The runtime gets judged by much harsher product constraints.&lt;/p&gt;

&lt;p&gt;A coding agent sits between the model and the operating system. It reads files, writes patches, spawns subprocesses, parses logs, watches test output, manages permissions, sometimes talks to IDEs, and often tries to recover from partial failure. That means the runtime is not just an execution engine. It becomes part of the product surface.&lt;/p&gt;

&lt;p&gt;If that surface is slow to start, users feel lag before the first useful token. If subprocess handling is flaky, test runs hang. If packaging is messy, install scripts fail on real laptops. If stack traces are noisy or state is spread across too many layers, incident diagnosis becomes miserable.&lt;/p&gt;

&lt;p&gt;This is why agentic tools punish the wrong runtime faster than ordinary apps do. A standard backend can hide a lot of runtime awkwardness behind containers, CI, and a stable deployment envelope. A local coding agent cannot. It runs in the user’s environment, touches their actual repo, and shells out to their actual toolchain. Every weak edge is now a product bug.&lt;/p&gt;

&lt;p&gt;A practical checklist for agent runtimes looks more like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cold start and interactive latency&lt;/li&gt;
&lt;li&gt;install and upgrade friction&lt;/li&gt;
&lt;li&gt;subprocess, pipe, and TTY behavior&lt;/li&gt;
&lt;li&gt;cross-platform filesystem correctness&lt;/li&gt;
&lt;li&gt;packaging and binary distribution&lt;/li&gt;
&lt;li&gt;sandbox compatibility and permission control&lt;/li&gt;
&lt;li&gt;observability during long-running sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That list tells you why the runtime discussion has shifted. It is not really about JavaScript versus Rust. It is about which stack gives the product team the most reliable control over a messy, local, tool-heavy workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bun Fits the Agent Shape Because It Collapses Tooling Layers
&lt;/h2&gt;

&lt;p&gt;Bun’s appeal in this space is not just speed. Speed matters, but the bigger story is &lt;strong&gt;surface area reduction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Traditional JavaScript CLI distribution often means stitching together Node, npm, a global install path, lockfile assumptions, version manager quirks, package resolution behavior, and sometimes native module surprises. That stack can work well, but it also creates a lot of places where a coding agent can fail before doing anything useful.&lt;/p&gt;

&lt;p&gt;Bun changes the shape of the problem by collapsing runtime, package manager, bundling assumptions, and CLI ergonomics into a tighter experience. For a consumer web app, that might be a nice productivity boost. For a local agent, it is much more than that. It is an attack on setup entropy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Friction Is a Product Metric
&lt;/h3&gt;

&lt;p&gt;Most developers underestimate how much agent adoption is governed by install quality. A coding tool does not get judged only after the tenth session. It gets judged during the first three minutes.&lt;/p&gt;

&lt;p&gt;If the install path looks like this, the tool already feels fragile:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install a specific Node version.&lt;/li&gt;
&lt;li&gt;Upgrade npm.&lt;/li&gt;
&lt;li&gt;Hope your shell profile exposes the correct global bin path.&lt;/li&gt;
&lt;li&gt;Work around one transitive dependency issue.&lt;/li&gt;
&lt;li&gt;Re-run because the postinstall step behaved differently on macOS and Linux.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is not a technical nuisance. That is user churn.&lt;/p&gt;

&lt;p&gt;A runtime that shortens the number of assumptions between download and first prompt wins disproportionate product value. Bun is attractive because it helps reduce that path length.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold Start Is Not a Benchmark Detail
&lt;/h3&gt;

&lt;p&gt;Interactive agents are judged like editors, not like web services. A one-second startup penalty is noticeable. Repeated across every invocation, context restore, or tool subprocess boundary, it becomes part of the perceived intelligence of the product.&lt;/p&gt;

&lt;p&gt;Users rarely say, "this runtime has poor startup characteristics." They say, "the tool feels heavy," or "I stopped using it for small tasks." That is the same complaint.&lt;/p&gt;

&lt;p&gt;When a runtime reduces boot cost and CLI overhead, it changes what kinds of tasks feel worth delegating to the agent. That is a product leverage point, not a micro-optimization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tighter Ownership Matters
&lt;/h3&gt;

&lt;p&gt;There is also an organizational angle here. If a vendor owns or strongly influences more of the runtime surface, they can close bugs across layers instead of endlessly routing around them.&lt;/p&gt;

&lt;p&gt;That matters for agent products because many failures do not live cleanly inside app code. They live in the seams between process execution, streaming, package resolution, permissions, and local OS behavior. A tighter runtime stack gives the product team more leverage on the exact places that hurt users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Rust Layer Matters More Than Marketing Suggests
&lt;/h2&gt;

&lt;p&gt;The phrase "written in Rust" gets abused in marketing, but in this context it points to something real. Agentic tools have a lot of systems-level failure modes.&lt;/p&gt;

&lt;p&gt;These tools need to manage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;subprocess lifetime and cancellation&lt;/li&gt;
&lt;li&gt;streaming stdout and stderr without deadlocks&lt;/li&gt;
&lt;li&gt;large filesystem walks&lt;/li&gt;
&lt;li&gt;low-latency parsing and transformation&lt;/li&gt;
&lt;li&gt;isolation boundaries&lt;/li&gt;
&lt;li&gt;memory behavior over long sessions&lt;/li&gt;
&lt;li&gt;crash resistance under ugly edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not glamorous product features, but they are core to the user experience. When part of the runtime foundation shifts closer to systems-language constraints, it usually means the team is trying to take tighter control over correctness and operational behavior.&lt;/p&gt;

&lt;p&gt;That does not mean Rust automatically fixes agent problems. It does mean the product team is less willing to accept vague, dynamic, layered failure modes in the runtime substrate.&lt;/p&gt;

&lt;p&gt;For coding agents, that is rational. The tool is already taking meaningful action on the developer’s machine. If it is going to edit files, launch commands, inspect repos, and potentially run inside restricted sandboxes, the underlying execution model needs to be boring in the best possible way.&lt;/p&gt;

&lt;p&gt;A useful way to think about the Rust angle is this: &lt;strong&gt;the closer your product gets to orchestrating the operating system, the more runtime implementation details start behaving like product features&lt;/strong&gt;. Safety, predictable concurrency, and resource control stop being internal engineering preferences. They become user trust factors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Runtime Decision Gets Real at the Subprocess Boundary
&lt;/h2&gt;

&lt;p&gt;If you want to evaluate an agent runtime honestly, stop looking at HTTP benchmarks and start looking at subprocess behavior.&lt;/p&gt;

&lt;p&gt;Coding agents are really subprocess orchestration engines with a model attached. They spend huge amounts of time running &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;rg&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;pnpm&lt;/code&gt;, &lt;code&gt;bun&lt;/code&gt;, &lt;code&gt;cargo&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;php artisan&lt;/code&gt;, &lt;code&gt;composer&lt;/code&gt;, &lt;code&gt;docker&lt;/code&gt;, and custom repo scripts. The agent is only as reliable as its ability to manage those tools under real-world noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Good Subprocess Support Needs to Handle
&lt;/h3&gt;

&lt;p&gt;A production-grade agent runtime needs predictable behavior for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;streaming large outputs without buffering disasters&lt;/li&gt;
&lt;li&gt;killing child processes cleanly on cancellation&lt;/li&gt;
&lt;li&gt;distinguishing normal exit, timeout, and signal termination&lt;/li&gt;
&lt;li&gt;handling interactive programs and pseudo-terminals&lt;/li&gt;
&lt;li&gt;preserving environment variables intentionally&lt;/li&gt;
&lt;li&gt;preventing hangs when stdout is noisy or stderr is bursty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not edge cases. They are everyday paths for coding tools.&lt;/p&gt;

&lt;p&gt;Here is the kind of execution wrapper an agent ends up needing, regardless of language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CommandResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timedOut&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CommandResult&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;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ignore&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exited&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="k"&gt;finally&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;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&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="nx"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timedOut&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&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;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&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 example is simple on purpose. Real agents add streaming callbacks, allowlists, structured events, output truncation, retry logic, and environment scoping. The point is not the syntax. The point is that the runtime must make this layer predictable enough that the product team can build policy on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure Behavior Is More Important Than Success Speed
&lt;/h3&gt;

&lt;p&gt;A fast happy path is nice. What matters more is whether the tool fails cleanly when the repo is weird.&lt;/p&gt;

&lt;p&gt;Common real failures include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a test command waiting for interactive input&lt;/li&gt;
&lt;li&gt;a child process spawning grandchildren that outlive cancellation&lt;/li&gt;
&lt;li&gt;environment drift between shell startup modes&lt;/li&gt;
&lt;li&gt;repo scripts that output gigabytes of logs&lt;/li&gt;
&lt;li&gt;tools that behave differently when no TTY exists&lt;/li&gt;
&lt;li&gt;Windows, WSL, and macOS path differences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the runtime makes those failures hard to observe or recover from, the agent will feel unreliable no matter how fast its benchmarks look.&lt;/p&gt;

&lt;p&gt;That is why the Bun story matters less as "faster JavaScript" and more as "a runtime stack willing to care deeply about CLI and systems ergonomics." That is the actual product need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sandboxing Changes the Runtime Conversation Completely
&lt;/h2&gt;

&lt;p&gt;Once an agent can run commands and edit files, sandboxing stops being a feature checkbox. It becomes a first-order architectural constraint.&lt;/p&gt;

&lt;p&gt;Anthropic’s sandboxing write-up on Claude Code is useful because it shows the real threat model: prompt injection, over-broad command access, accidental data exposure, and risky tool execution in a local environment. An agent runtime that cannot cooperate cleanly with isolation controls is a weak foundation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sandboxing Needs Runtime Cooperation
&lt;/h3&gt;

&lt;p&gt;The runtime does not need to provide the entire sandbox by itself, but it must play well with the layers around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;operating system sandbox primitives&lt;/li&gt;
&lt;li&gt;containerized execution&lt;/li&gt;
&lt;li&gt;command allowlists&lt;/li&gt;
&lt;li&gt;temp directory isolation&lt;/li&gt;
&lt;li&gt;file permission boundaries&lt;/li&gt;
&lt;li&gt;policy checks before dangerous actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A coding agent often needs an execution funnel that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User request
  -&amp;gt; planner
  -&amp;gt; policy check
  -&amp;gt; tool selection
  -&amp;gt; sandbox / permission gate
  -&amp;gt; subprocess execution
  -&amp;gt; structured result
  -&amp;gt; model reflection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the runtime makes subprocess control, environment shaping, or temporary filesystem isolation awkward, the product team ends up fighting the platform instead of implementing safety policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Native Extensions and System Interfaces Still Matter
&lt;/h3&gt;

&lt;p&gt;This is where the runtime choice gets uncomfortable. Pure developer experience is not enough. Agent tools often end up touching native capabilities or low-level OS interfaces indirectly through libraries, platform APIs, or security wrappers.&lt;/p&gt;

&lt;p&gt;That means compatibility still matters. Node remains strong here because of ecosystem inertia. There are simply more libraries, more edge-case integrations, and more enterprise-tested patterns.&lt;/p&gt;

&lt;p&gt;Bun can still be the right choice, but the burden shifts. If your agent needs a narrow, predictable, tightly controlled stack, Bun’s tradeoffs can be excellent. If your tool depends on long-tail packages, ancient build assumptions, or deeply entrenched enterprise environments, Node’s ecosystem gravity is still very real.&lt;/p&gt;

&lt;p&gt;That is why this is not a religion war. It is a product topology question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packaging, Updates, and Operational Debugging Decide Who Wins
&lt;/h2&gt;

&lt;p&gt;The least glamorous parts of developer tooling usually decide the winner.&lt;/p&gt;

&lt;p&gt;A coding agent is not done once it works on the maintainer’s laptop. It has to update cleanly, recover from partial installs, report meaningful failure states, and leave enough diagnostics behind that support engineers can tell whether the problem was the model, the runtime, the repo, or the OS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Packaging Is Part of Trust
&lt;/h3&gt;

&lt;p&gt;A bad packaging story creates a trust tax. Users become hesitant to upgrade. Teams pin old versions. Bug reports get contaminated by environment drift.&lt;/p&gt;

&lt;p&gt;The strongest developer tools minimize that drift by making updates boring. That usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one obvious install path&lt;/li&gt;
&lt;li&gt;one obvious upgrade path&lt;/li&gt;
&lt;li&gt;minimal global dependency assumptions&lt;/li&gt;
&lt;li&gt;small number of runtime layers&lt;/li&gt;
&lt;li&gt;clear version reporting and health checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is another reason integrated runtime stacks are attractive. They reduce the number of actors that can disagree with each other during install or upgrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging Needs Structured Events, Not Just Logs
&lt;/h3&gt;

&lt;p&gt;Agent incidents are usually multi-layer failures. A user says, "the tool froze while reviewing a PR." That can mean any of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;model request stalled&lt;/li&gt;
&lt;li&gt;subprocess hung&lt;/li&gt;
&lt;li&gt;sandbox denied access&lt;/li&gt;
&lt;li&gt;file walker hit a symlink trap&lt;/li&gt;
&lt;li&gt;output parser blocked on a stream&lt;/li&gt;
&lt;li&gt;background task died without surfacing an error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your runtime and harness do not emit structured execution events, debugging becomes guesswork.&lt;/p&gt;

&lt;p&gt;A serious agent product should be able to record a session trail like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;command selected&lt;/li&gt;
&lt;li&gt;policy decision made&lt;/li&gt;
&lt;li&gt;subprocess started&lt;/li&gt;
&lt;li&gt;timeout threshold reached&lt;/li&gt;
&lt;li&gt;cancellation signal sent&lt;/li&gt;
&lt;li&gt;subprocess exit observed&lt;/li&gt;
&lt;li&gt;stderr classified&lt;/li&gt;
&lt;li&gt;model resumed with summarized result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not overengineering. It is what makes support and reliability work possible once real developers adopt the tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ecosystem Tradeoff Still Has Teeth
&lt;/h3&gt;

&lt;p&gt;This is where Node keeps its strongest argument. Mature ecosystems are full of ugly compatibility knowledge that newer stacks do not have yet. For some products, that matters more than startup speed or surface reduction.&lt;/p&gt;

&lt;p&gt;A rough decision split looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bun is attractive when you want:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;low-friction CLI installs&lt;/li&gt;
&lt;li&gt;fast interactive startup&lt;/li&gt;
&lt;li&gt;tighter control over distribution&lt;/li&gt;
&lt;li&gt;fewer runtime layers to debug&lt;/li&gt;
&lt;li&gt;an execution model optimized for modern JS tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Node is safer when you need:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maximum compatibility with older packages&lt;/li&gt;
&lt;li&gt;enterprise environments with conservative assumptions&lt;/li&gt;
&lt;li&gt;broader support for obscure integrations&lt;/li&gt;
&lt;li&gt;fewer unknowns around long-tail ecosystem behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the real comparison. Not speed chart versus speed chart. Just which runtime gives the product team fewer failure classes they cannot control.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude Code’s Runtime Choice Actually Teaches
&lt;/h2&gt;

&lt;p&gt;The lesson from Claude Code is not that Bun is the future of everything. The lesson is that &lt;strong&gt;agentic tools are forcing runtime decisions to become product decisions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When your tool is terminal-native, model-driven, subprocess-heavy, and safety-sensitive, the runtime is no longer just an implementation detail. It shapes install quality, latency, command execution, sandbox design, and incident response. That is a different bar than most app developers are used to.&lt;/p&gt;

&lt;p&gt;My recommendation is straightforward.&lt;/p&gt;

&lt;p&gt;If you are building a coding agent or local AI operator, evaluate runtimes in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does it make first-run success easier?&lt;/li&gt;
&lt;li&gt;Does it make subprocess execution predictable?&lt;/li&gt;
&lt;li&gt;Does it cooperate with sandboxing and permission policy?&lt;/li&gt;
&lt;li&gt;Does it simplify packaging and updates?&lt;/li&gt;
&lt;li&gt;Does it make operational debugging easier under failure?&lt;/li&gt;
&lt;li&gt;Only then ask about broad ecosystem preference or benchmark bragging rights.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By that standard, Bun is a serious runtime choice for agentic tooling, and the Bun plus Rust direction makes strategic sense. It is aiming at exactly the surfaces that hurt local AI tools most.&lt;/p&gt;

&lt;p&gt;But do not copy the move blindly. If your product depends on ecosystem breadth more than runtime control, Node may still be the better engineering choice. If your product lives or dies on install quality, CLI responsiveness, and subprocess reliability, the Bun-style bet becomes much easier to justify.&lt;/p&gt;

&lt;p&gt;The clean decision rule is this: &lt;strong&gt;pick the runtime that reduces user-facing operational friction, not the one that wins the loudest benchmark debate&lt;/strong&gt;. For coding agents, that usually means the runtime that makes the tool easier to install, safer to run, and less mysterious to debug when it fails.&lt;/p&gt;




&lt;p&gt;Read the full post on QCode: &lt;a href="https://qcode.in/claude-code-on-bun-agentic-tools-runtime-choices/" rel="noopener noreferrer"&gt;https://qcode.in/claude-code-on-bun-agentic-tools-runtime-choices/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>bunjs</category>
      <category>rust</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
