<?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: Shakil Alam</title>
    <description>The latest articles on DEV Community by Shakil Alam (@itxshakil).</description>
    <link>https://dev.to/itxshakil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F160872%2F81c0d832-7a90-4c62-9c2a-157bb71a6772.jpg</url>
      <title>DEV Community: Shakil Alam</title>
      <link>https://dev.to/itxshakil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/itxshakil"/>
    <language>en</language>
    <item>
      <title>How We Implemented Content Security Policy (CSP) in Our Laravel App</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Fri, 08 May 2026 13:08:47 +0000</pubDate>
      <link>https://dev.to/itxshakil/how-we-implemented-content-security-policy-csp-in-our-laravel-app-mh4</link>
      <guid>https://dev.to/itxshakil/how-we-implemented-content-security-policy-csp-in-our-laravel-app-mh4</guid>
      <description>&lt;p&gt;Our pentest report had one line that stopped us cold:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Application does not implement Content-Security-Policy headers. XSS payloads executed without restriction."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We had Sanctum, CSRF tokens, input validation — all the standard Laravel security checklist items. But we had no CSP. And without it, a single successful XSS attack could exfiltrate session cookies, inject malicious scripts, or silently redirect users to attacker-controlled pages — all from our own domain.&lt;/p&gt;

&lt;p&gt;This is the story of how we added CSP to a production Laravel application without breaking anything, how we built a violation reporting pipeline, and the things we wish we'd known before starting.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's in this guide&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is written to be useful regardless of where you're starting from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never heard of CSP?&lt;/strong&gt; Start from the top. The first two sections give you the mental model before any code appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know what CSP is but haven't implemented it?&lt;/strong&gt; Jump to The Implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already running CSP and want to tighten it or add reporting?&lt;/strong&gt; Jump to CSP Violation Reporting or the Pre-Enforcement Checklist.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Content Security Policy Actually Does
&lt;/h2&gt;

&lt;p&gt;When a browser loads a page, it executes whatever scripts, styles, fonts, and images are on it — regardless of where they came from. That's what makes XSS dangerous: if an attacker manages to inject a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag into your HTML, the browser runs it without question.&lt;/p&gt;

&lt;p&gt;CSP is an HTTP response header that changes this. It tells the browser: &lt;em&gt;"Only trust resources from these specific sources. Anything else — block it."&lt;/em&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;Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; object-src 'none'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that header in place, even if an attacker injects a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, the browser refuses to execute it. The tag has no valid &lt;strong&gt;nonce&lt;/strong&gt; — a cryptographically random token generated fresh for each request. Scripts carrying the matching nonce run. Everything else is blocked.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;New to this?&lt;/strong&gt; MDN has the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy" rel="noopener noreferrer"&gt;definitive CSP reference&lt;/a&gt; if you want to go deeper on any specific directive.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Is a Nonce?
&lt;/h2&gt;

&lt;p&gt;You'll see the word "nonce" throughout this post, so let's settle it quickly before writing any code.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;nonce&lt;/strong&gt; (short for &lt;em&gt;number used once&lt;/em&gt;) is a random string generated fresh on every single page load. The server puts it in the CSP header, and also stamps it onto every &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block it trusts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the HTTP header the server sends:&lt;/span&gt;
&lt;span class="nx"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Security&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nonce-k8Hv2mXpQ3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// On the script tag in your HTML:&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;k8Hv2mXpQ3&lt;/span&gt;&lt;span class="dl"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser sees both, checks they match, and runs the script. If an attacker injects a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, it has no nonce — or the wrong one — so it gets blocked. Since the nonce changes on every request, an attacker who somehow saw yesterday's nonce can't reuse it today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just use a static secret key?&lt;/strong&gt; Because a static value could be cached, leaked in logs, or extracted from your source code. A nonce only needs to survive one request. After that, it's worthless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a nonce is not:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is not a replacement for input sanitisation. A nonce stops injected scripts from &lt;em&gt;running&lt;/em&gt;. It doesn't stop them from being &lt;em&gt;stored&lt;/em&gt; in your database.&lt;/li&gt;
&lt;li&gt;It is not encryption. Anyone who can see the page source can see the nonce — that's fine, because it expires the moment the request ends.&lt;/li&gt;
&lt;li&gt;It is not a CSRF token. They look similar in concept but solve completely different problems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. When you see &lt;code&gt;nonce="{{ $cspNonce }}"&lt;/code&gt; in templates below, it simply means: &lt;em&gt;"the server trusts this specific block."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Naive Approach (and Why It Fails)
&lt;/h2&gt;

&lt;p&gt;Most teams discover CSP when a security audit flags it, then reach for the quickest fix: a static header in &lt;code&gt;nginx.conf&lt;/code&gt; or &lt;code&gt;apache.conf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This works for about five minutes. Then the inline JavaScript breaks. Alpine.js stops. Livewire stops. Any &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block that doesn't come from a separate file gets blocked. The knee-jerk reaction is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;script-src 'self' 'unsafe-inline'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Everything works again. And CSP is now completely useless — &lt;code&gt;'unsafe-inline'&lt;/code&gt; means &lt;em&gt;any&lt;/em&gt; inline script can run, including one an attacker injected.&lt;/p&gt;

&lt;p&gt;The correct approach is a &lt;strong&gt;nonce-based CSP generated per request inside Laravel&lt;/strong&gt;, where every trusted inline script carries a token only the server knows. That's what we built.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mindset Shift: Write CSP-Friendly Code
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of middleware, you need to change how you write templates. CSP doesn't just enforce a header — it enforces discipline about where your code lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move JavaScript out of your HTML.&lt;/strong&gt; Event handler attributes are the main offender:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ❌ This is blocked by any sensible CSP --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"submitForm()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitForm&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- ✅ This is CSP-friendly — JS lives in an external file --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"submit-btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/js/form.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Move styles out of your HTML too.&lt;/strong&gt; The &lt;code&gt;style=""&lt;/code&gt; attribute on elements cannot carry a nonce — there's no mechanism to whitelist individual instances. Your only options with a strict policy are "allow all of them" or "block all of them." Build the habit of using CSS classes from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For inline code you genuinely can't move&lt;/strong&gt; — a config value JavaScript needs, critical above-the-fold CSS — the nonce is your escape hatch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;nonce=&lt;/span&gt;&lt;span class="s"&gt;"{{ $cspNonce }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_ENV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{ config('app.env') }}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A nonce is generated fresh on every request. An attacker can't predict it. A script they inject has no nonce, so it gets blocked even if everything else fails.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Never hardcode a nonce, never reuse one. Generate it with &lt;code&gt;random_bytes()&lt;/code&gt; — not &lt;code&gt;uniqid()&lt;/code&gt;, not &lt;code&gt;md5(time())&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Just want the output without the reading?&lt;/strong&gt; I built a free &lt;a href="https://blog.shakiltech.com/tools/laravel-csp-generator" rel="noopener noreferrer"&gt;Laravel CSP Generator&lt;/a&gt; — toggle your CDNs and integrations, and it generates the full policy string, the PHP middleware method, and an adaptive pre-enforcement checklist live. Come back here for the why behind each decision.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;We put all security headers in one &lt;code&gt;SecureHeadersMiddleware&lt;/code&gt; rather than spreading logic across &lt;code&gt;nginx.conf&lt;/code&gt;, &lt;code&gt;.htaccess&lt;/code&gt;, and multiple middleware files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Generate the Nonce and Share It
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:middleware SecureHeadersMiddleware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&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="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\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="nc"&gt;Illuminate\Support\Facades\Vite&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\View&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;SecureHeadersMiddleware&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Disable in local/testing to avoid log noise from developer browsers&lt;/span&gt;
    &lt;span class="c1"&gt;// full of extensions. Automatically driven by the current environment.&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$allowReporting&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="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;allowReporting&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'local'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'testing'&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;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="nv"&gt;$nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;random_bytes&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="c1"&gt;// Share with all Blade templates automatically — no manual passing needed&lt;/span&gt;
        &lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cspNonce'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Tell Vite to inject this nonce on its bootstrapping inline script.&lt;/span&gt;
        &lt;span class="c1"&gt;// Skip this and @vite() silently breaks under a strict CSP.&lt;/span&gt;
        &lt;span class="nc"&gt;Vite&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;useCspNonce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$nonce&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addContentSecurityPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$nonce&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addClickjackingProtection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&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;addStrictTransportSecurity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addMiscSecurityHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&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;removeUnwantedHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;allowReporting&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;supportsReportTo&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addCspReportingEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&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="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;Two ordering rules matter here. The nonce must be generated &lt;em&gt;before&lt;/em&gt; &lt;code&gt;$next($request)&lt;/code&gt; because the view renders during that call. The headers are set &lt;em&gt;after&lt;/em&gt; because they need the response object. Swap either and things break silently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Define the CSP Policy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;addContentSecurityPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="nv"&gt;$response&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;$nonce&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;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$isLocal&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"default-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"script-src 'self' 'nonce-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$nonce&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://embed.tawk.to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"style-src 'self' 'nonce-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$nonce&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' https://fonts.googleapis.com https://cdnjs.cloudflare.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"style-src-elem 'self' 'nonce-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$nonce&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' https://fonts.googleapis.com https://cdnjs.cloudflare.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"style-src-attr 'none'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"font-src 'self' data: https://fonts.gstatic.com https://cdnjs.cloudflare.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"img-src 'self' data: https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://your-cdn.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"frame-src 'self' https://your-video-host.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="c1"&gt;// Vite's HMR uses a WebSocket on localhost — only needed in local dev.&lt;/span&gt;
        &lt;span class="c1"&gt;// Never ship localhost entries to production.&lt;/span&gt;
        &lt;span class="s2"&gt;"connect-src 'self'"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$isLocal&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;" ws://localhost:5173 wss://localhost:5173"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

        &lt;span class="s2"&gt;"object-src 'none'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"base-uri 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"frame-ancestors 'none'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// upgrade-insecure-requests on a local HTTP server triggers confusing&lt;/span&gt;
    &lt;span class="c1"&gt;// mixed-content errors that have nothing to do with your code.&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;$isLocal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$policy&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'upgrade-insecure-requests'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;allowReporting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$policy&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'report-uri /api/csp-violation-report'&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="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;'Content-Security-Policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;implode&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="nv"&gt;$policy&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Directive reference — what each one actually does:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directive&lt;/th&gt;
&lt;th&gt;What it controls&lt;/th&gt;
&lt;th&gt;Our value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fallback for any type not explicitly listed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'self'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;script-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JavaScript execution&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'self'&lt;/code&gt; + nonce + CDN allowlist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;style-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSS &lt;code&gt;@import&lt;/code&gt; rules within stylesheets; base fallback&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'self'&lt;/code&gt; + nonce + CDN allowlist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;style-src-elem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;link rel="stylesheet"&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'self'&lt;/code&gt; + nonce + CDN allowlist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;style-src-attr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inline &lt;code&gt;style=""&lt;/code&gt; attributes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'none'&lt;/code&gt; (blocked entirely)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;font-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Font files&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'self'&lt;/code&gt; + data URIs + Google Fonts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;img-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Images&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'self'&lt;/code&gt; + data URIs + CDN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frame-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; sources&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'self'&lt;/code&gt; + trusted video host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connect-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fetch()&lt;/code&gt;, XHR, WebSockets, SSE&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'self'&lt;/code&gt; + localhost WS in dev&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;object-src&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browser plugins (Flash, Java)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'none'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;base-uri&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;&amp;lt;base&amp;gt;&lt;/code&gt; tag's &lt;code&gt;href&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'self'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frame-ancestors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Who can embed this page in an iframe&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'none'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;default-src 'self'&lt;/code&gt;&lt;/strong&gt; means anything not covered by a specific directive falls back to same-origin only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;script-src&lt;/code&gt;&lt;/strong&gt; is where most of the work happens. The nonce is per-request, cryptographically random, and unpredictable. In CSP Level 3, the presence of a nonce automatically overrides &lt;code&gt;'unsafe-inline'&lt;/code&gt; for supporting browsers — so even if you add it as a temporary fallback, modern browsers ignore it in favour of nonce validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;connect-src&lt;/code&gt;&lt;/strong&gt; controls everything your JavaScript can talk to: &lt;code&gt;fetch()&lt;/code&gt;, &lt;code&gt;XMLHttpRequest&lt;/code&gt;, WebSockets, and Server-Sent Events. Vite's Hot Module Replacement uses a WebSocket on &lt;code&gt;localhost:5173&lt;/code&gt; to push changes to your browser during development. Without it in &lt;code&gt;connect-src&lt;/code&gt;, HMR fails silently and you're doing hard refreshes all day. In production, that entry must not appear — hence the environment check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;object-src 'none'&lt;/code&gt;&lt;/strong&gt; blocks Flash and all other browser plugins. There's no reason to allow these in 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;base-uri 'self'&lt;/code&gt;&lt;/strong&gt; prevents an attacker from injecting &lt;code&gt;&amp;lt;base href="https://evil.com"&amp;gt;&lt;/code&gt;, which would silently redirect all relative URLs — links, form actions, script paths — to their server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;frame-ancestors 'none'&lt;/code&gt;&lt;/strong&gt; is clickjacking protection at the CSP level: your pages cannot be embedded in iframes on other domains. Stronger than &lt;code&gt;X-Frame-Options&lt;/code&gt; for modern browsers; we keep &lt;code&gt;X-Frame-Options&lt;/code&gt; too for legacy ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;upgrade-insecure-requests&lt;/code&gt;&lt;/strong&gt; tells the browser to automatically upgrade HTTP sub-resource requests to HTTPS. We skip it in local development because it causes confusing mixed-content errors on a plain HTTP dev server.&lt;/p&gt;

&lt;h4&gt;
  
  
  Deep Dive: &lt;code&gt;style-src&lt;/code&gt;, &lt;code&gt;style-src-elem&lt;/code&gt;, and &lt;code&gt;style-src-attr&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Most developers assume these three are aliases for the same thing and only set &lt;code&gt;style-src&lt;/code&gt;. They're not — and that misunderstanding is what causes hours of debugging when styles randomly break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one-line summary:&lt;/strong&gt; &lt;code&gt;style-src&lt;/code&gt; is the fallback. &lt;code&gt;style-src-elem&lt;/code&gt; controls &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks and &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags. &lt;code&gt;style-src-attr&lt;/code&gt; controls &lt;code&gt;style=""&lt;/code&gt; attributes on elements. Set all three explicitly, or the browser decides how to inherit, which is rarely what you want.&lt;/p&gt;

&lt;p&gt;Here's the breakdown:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;style-src&lt;/code&gt;&lt;/strong&gt; is the base rule that covers everything CSS-related when the more specific directives aren't set. Even when you do set the others, &lt;code&gt;style-src&lt;/code&gt; still governs one thing they don't: &lt;strong&gt;CSS &lt;code&gt;@import&lt;/code&gt; rules inside your own stylesheets&lt;/strong&gt;. &lt;code&gt;@import url('https://fonts.googleapis.com/...')&lt;/code&gt; is controlled by &lt;code&gt;style-src&lt;/code&gt;, not &lt;code&gt;style-src-elem&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;style-src-elem&lt;/code&gt;&lt;/strong&gt; controls two things: &lt;code&gt;&amp;lt;link rel="stylesheet"&amp;gt;&lt;/code&gt; tags and &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks. The important detail is that &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks &lt;em&gt;can&lt;/em&gt; carry a nonce, so you can precisely whitelist only the inline styles you wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{{-- Trusted — nonce matches the header --}}
&lt;span class="nt"&gt;&amp;lt;style &lt;/span&gt;&lt;span class="na"&gt;nonce=&lt;/span&gt;&lt;span class="s"&gt;"{{ $cspNonce }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/img/hero.jpg')&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;

{{-- Blocked — no nonce --}}
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;.injected&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;red&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;External stylesheets via &lt;code&gt;&amp;lt;link href="..."&amp;gt;&lt;/code&gt; don't need a nonce — they're controlled by the domain allowlist. Nonces only apply to inline content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;style-src-attr&lt;/code&gt;&lt;/strong&gt; controls only the inline &lt;code&gt;style=""&lt;/code&gt; attribute on HTML elements — and this is the problem child. &lt;code&gt;style=""&lt;/code&gt; attributes &lt;strong&gt;cannot carry a nonce&lt;/strong&gt;. There is no way to whitelist specific ones. Your only two options are &lt;code&gt;'unsafe-inline'&lt;/code&gt; (trusts all of them, including anything injected) or &lt;code&gt;'none'&lt;/code&gt; (blocks all of them). We use &lt;code&gt;'none'&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;style-src       → CSS @import in files + base fallback  → 'self' + nonce + CDN
style-src-elem  → &amp;lt;link&amp;gt; tags + &amp;lt;style&amp;gt; blocks          → nonce-gated
style-src-attr  → style="" attributes                   → 'none', blocked entirely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Before you enable this:&lt;/strong&gt; audit your JavaScript-driven UI libraries. Drag-and-drop, tooltips, and modals commonly set &lt;code&gt;style="top: 48px; left: 200px"&lt;/code&gt; dynamically — every one of them will break under &lt;code&gt;style-src-attr 'none'&lt;/code&gt;. Run in report-only mode first and your logs will tell you exactly which libraries are the offenders before anything breaks in production.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: Use the Nonce in Blade
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{{-- Vite assets — no nonce attribute needed on the tag itself.
     Vite::useCspNonce() in the middleware handles the inline bootstrapper. --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])

{{-- Inline script blocks you write manually --}}
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;nonce=&lt;/span&gt;&lt;span class="s"&gt;"{{ $cspNonce }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$appConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

{{-- Inline style blocks --}}
&lt;span class="nt"&gt;&amp;lt;style &lt;/span&gt;&lt;span class="na"&gt;nonce=&lt;/span&gt;&lt;span class="s"&gt;"{{ $cspNonce }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/img/hero.jpg')&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;

{{-- Livewire v2 --}}
@livewireScripts(['nonce' =&amp;gt; $cspNonce])

{{-- Livewire v3 --}}
@livewireScriptConfig(['nonce' =&amp;gt; $cspNonce])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One thing to be clear on:&lt;/strong&gt; &lt;code&gt;&amp;lt;script src="..."&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;link rel="stylesheet" href="..."&amp;gt;&lt;/code&gt; tags pointing to external files do &lt;strong&gt;not&lt;/strong&gt; need a nonce attribute. The browser permits them if their domain is on the allowlist. Nonces exist purely for inline content that has no domain to verify against.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Add the Rest of the Security Header Stack
&lt;/h3&gt;

&lt;p&gt;CSP is one layer. The same middleware handles the remaining browser security headers — each in a focused method:&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;addClickjackingProtection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Legacy browsers fall back to this; modern ones use frame-ancestors from CSP&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;'X-Frame-Options'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DENY'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;addStrictTransportSecurity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="nv"&gt;$response&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;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Check X-Forwarded-Proto too — without this, HSTS is never sent when&lt;/span&gt;
    &lt;span class="c1"&gt;// you're behind a load balancer that terminates SSL before Laravel sees the request&lt;/span&gt;
    &lt;span class="nv"&gt;$isHttps&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;isSecure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;strcasecmp&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="nv"&gt;$request&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;'X-Forwarded-Proto'&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="s1"&gt;'https'&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="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;$isHttps&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;$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;'Strict-Transport-Security'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max-age=31536000; includeSubDomains; preload'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;addMiscSecurityHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Prevents browsers from MIME-sniffing a response away from the declared content-type&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;'X-Content-Type-Options'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'nosniff'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Controls what's sent in the Referer header on navigation&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;'Referrer-Policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'strict-origin-when-cross-origin'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Deprecated and ignored by modern browsers, but harmless to include&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;'X-XSS-Protection'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1; mode=block'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Restrict access to sensitive browser APIs&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;'Permissions-Policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'geolocation=(), microphone=(self), camera=(self), payment=(), fullscreen=*'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;removeUnwantedHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Both calls are needed: Symfony's header management and PHP's header() function&lt;/span&gt;
    &lt;span class="c1"&gt;// operate at different levels. Without both, X-Powered-By: PHP/8.x can still leak.&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'X-Powered-By'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Server'&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;$header&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="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$header&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;header_remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$header&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Register the Middleware
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Laravel 11 — bootstrap/app.php&lt;/span&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="kt"&gt;Middleware&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;web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;\App\Http\Middleware\SecureHeadersMiddleware&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;span class="c1"&gt;// Laravel 10 — app/Http/Kernel.php&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$middlewareGroups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'web'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="nc"&gt;\App\Http\Middleware\SecureHeadersMiddleware&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;Attaching it to the &lt;code&gt;web&lt;/code&gt; group means your JSON API routes — which don't render Blade views — don't receive an unnecessary CSP header. For high-throughput APIs that's a meaningful separation.&lt;/p&gt;




&lt;h2&gt;
  
  
  CSP Violation Reporting — Your Early Warning System 🚨
&lt;/h2&gt;

&lt;p&gt;Running CSP without a reporting endpoint is like setting a burglar alarm with no monitoring. You need to know when the policy blocks something — to catch misconfigurations before users do, and to detect real injection attempts.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a Violation Report Looks Like
&lt;/h3&gt;

&lt;p&gt;When a browser blocks something, it POSTs a JSON payload to your report endpoint. Here's what that looks like:&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;"csp-report"&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;"document-uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://yourapp.com/dashboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"referrer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"violated-directive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"script-src-elem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"effective-directive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"script-src-elem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"original-policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default-src 'self'; script-src 'self' 'nonce-abc123'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"blocked-uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://evil.com/injected.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status-code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&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;The &lt;code&gt;blocked-uri&lt;/code&gt; tells you what was blocked. The &lt;code&gt;violated-directive&lt;/code&gt; tells you which rule caught it. Together they tell you immediately whether this is a legitimate resource you forgot to allowlist or something that should never have been there.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Hosted alternative:&lt;/strong&gt; If you'd rather not build your own reporting pipeline, &lt;a href="https://report-uri.com" rel="noopener noreferrer"&gt;report-uri.com&lt;/a&gt; by Scott Helme is a dedicated CSP report collection and analysis service. It handles noise filtering, dashboards, and alerting for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Route
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/api.php&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;'/csp-violation-report'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;CspReportController&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;'store'&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;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'throttle:5'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;throttle:5&lt;/code&gt; limits to 5 reports per minute per IP. Without rate limiting, an attacker can flood your logging infrastructure by triggering violations in a loop — exhausting disk space or burying real threats in noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Controller
&lt;/h3&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="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&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="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers\Api&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="nc"&gt;Illuminate\Support\Facades\Log&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\RateLimiter&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;CspReportController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&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;getContent&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="nv"&gt;$report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'csp-report'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nv"&gt;$body&lt;/span&gt; &lt;span class="o"&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="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$report&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;noContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$ip&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;ip&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$blockedUri&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;$report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'blocked-uri'&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="nv"&gt;$violatedDirective&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;$report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'violated-directive'&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="c1"&gt;// Browser extensions are the biggest source of CSP noise.&lt;/span&gt;
        &lt;span class="c1"&gt;// Ad blockers, password managers, and DevTools extensions all fire reports.&lt;/span&gt;
        &lt;span class="c1"&gt;// They're not actionable — filter them immediately.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'chrome-extension://'&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_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'moz-extension://'&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_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'safari-extension://'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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;noContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Low-risk: usually your own code or a library doing something non-standard.&lt;/span&gt;
        &lt;span class="c1"&gt;// Review weekly, not immediately.&lt;/span&gt;
        &lt;span class="nv"&gt;$lowRiskUris&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'about:blank'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'blob:'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data:'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'inline'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eval'&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="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$blockedUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$lowRiskUris&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;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'csp_low'&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;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Low-risk CSP violation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'report'&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;noContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// High-risk: an external domain, an unknown script, potentially injected content.&lt;/span&gt;
        &lt;span class="c1"&gt;// Log with full context. Alert if sustained.&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;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'csp_high'&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;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'High-risk CSP violation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'report'&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;checkForThresholdAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$blockedUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$violatedDirective&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;noContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;checkForThresholdAlert&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;$ip&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;$uri&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;$directive&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Same IP, same directive, 10+ times in 60 seconds = probe, not noise.&lt;/span&gt;
        &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"csp_alert:&lt;/span&gt;&lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$directive&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="s2"&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="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;tooManyAttempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&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="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;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CSP threshold exceeded — possible active attack'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'ip'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'uri'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'directive'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$directive&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="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Log Channels
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/logging.php&lt;/span&gt;
&lt;span class="s1"&gt;'csp_low'&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;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'single'&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="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logs/csp-low-risk.log'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'level'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'info'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;

&lt;span class="s1"&gt;'csp_high'&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;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'single'&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="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logs/csp-high-risk.log'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'level'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'warning'&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;Separate files let you set different retention policies — low-risk logs rotate weekly, high-risk logs stay for 90 days. In production, wire &lt;code&gt;Log::alert()&lt;/code&gt; into Slack or PagerDuty so a spike in high-risk violations at 2 AM wakes someone up.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Modern Report-To Header
&lt;/h3&gt;

&lt;p&gt;For Chrome 70+, Edge 79+, and Firefox 60+, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Reporting_API" rel="noopener noreferrer"&gt;Reporting API&lt;/a&gt; (&lt;code&gt;Report-To&lt;/code&gt; header) is more efficient than &lt;code&gt;report-uri&lt;/code&gt; — reports are batched, and the endpoint is cached so the browser sends reports even from pages that didn't include the header.&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;supportsReportTo&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;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$ua&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'User-Agent'&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'/(Chrome\/([7-9][0-9]|[1-9][0-9]{2,}))|(Firefox\/(6[0-9]|[7-9][0-9]|[1-9][0-9]{2,}))|(Edg\/([7-9][0-9]|[1-9][0-9]{2,}))|(Version\/(1[3-9]|[2-9][0-9])) Safari\//'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$ua&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;addCspReportingEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="nv"&gt;$response&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;$reportTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'group'&lt;/span&gt;              &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'csp-endpoint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'max_age'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10886400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 126 days — browser caches this endpoint&lt;/span&gt;
        &lt;span class="s1"&gt;'endpoints'&lt;/span&gt;          &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/csp-violation-report'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'include_subdomains'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="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;'Report-To'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$reportTo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON_UNESCAPED_SLASHES&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;We keep &lt;code&gt;report-uri&lt;/code&gt; in the CSP policy as a fallback for browsers that don't support the Reporting API. Both can coexist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-Enforcement Checklist
&lt;/h2&gt;

&lt;p&gt;Run through this before switching from report-only to enforcement mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;Vite::useCspNonce($nonce)&lt;/code&gt; is called &lt;em&gt;before&lt;/em&gt; &lt;code&gt;$next($request)&lt;/code&gt; in the middleware&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;@vite()&lt;/code&gt;, &lt;code&gt;@livewireScripts()&lt;/code&gt;, and &lt;code&gt;@livewireScriptConfig()&lt;/code&gt; all pass the nonce&lt;/li&gt;
&lt;li&gt;[ ] Every hand-written inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block has &lt;code&gt;nonce="{{ $cspNonce }}"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Every hand-written inline &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block has &lt;code&gt;nonce="{{ $cspNonce }}"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;onclick=""&lt;/code&gt;, &lt;code&gt;onsubmit=""&lt;/code&gt;, and other event handler attributes are removed from templates&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;style=""&lt;/code&gt; attributes are replaced with CSS classes (or the library using them is documented)&lt;/li&gt;
&lt;li&gt;[ ] All CDN domains you load from are in the appropriate allowlist&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;connect-src&lt;/code&gt; includes your API endpoints, WebSocket hosts, and analytics targets&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;connect-src&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; include &lt;code&gt;localhost&lt;/code&gt; in your production policy&lt;/li&gt;
&lt;li&gt;[ ] The violation report endpoint is live, throttled, and log channels are configured&lt;/li&gt;
&lt;li&gt;[ ] Report-only mode has run in production for at least one week with no new high-risk violations&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Deploy Safely: Start in Report-Only Mode
&lt;/h2&gt;

&lt;p&gt;If you're adding CSP to an existing app, don't jump straight to enforcement. Swap the header name 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="c1"&gt;// Change this:&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;'Content-Security-Policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;implode&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="nv"&gt;$policy&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// To this, temporarily:&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;'Content-Security-Policy-Report-Only'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;implode&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="nv"&gt;$policy&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser reports violations but blocks nothing. Run it in production for one to two weeks and watch &lt;code&gt;csp-high-risk.log&lt;/code&gt;. Every entry is either a resource to allowlist or evidence of something that should never have been there. Once the log goes quiet, flip back to &lt;code&gt;Content-Security-Policy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This step is what separates a smooth rollout from an angry support queue.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verify your headers:&lt;/strong&gt; Use &lt;a href="https://securityheaders.com" rel="noopener noreferrer"&gt;securityheaders.com&lt;/a&gt; to grade your response headers. Use Google's &lt;a href="https://csp-evaluator.withgoogle.com/" rel="noopener noreferrer"&gt;CSP Evaluator&lt;/a&gt; to check your policy for common weaknesses. Both are free.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What We'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Drop &lt;code&gt;X-XSS-Protection&lt;/code&gt;.&lt;/strong&gt; It's deprecated and ignored by modern browsers. Older IE versions had a bug where it could be exploited. We kept it because it doesn't actively hurt anything, but new implementations shouldn't bother.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate concerns if your team is large.&lt;/strong&gt; Bundling all headers in one middleware works well for us, but a dedicated &lt;code&gt;CspMiddleware&lt;/code&gt; is easier to unit test and simpler to replace later. &lt;a href="https://github.com/spatie/laravel-csp" rel="noopener noreferrer"&gt;Spatie's laravel-csp&lt;/a&gt; package gives you a structured, per-policy-class approach with first-class test support if you want to skip the custom build entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use hashes for truly static inline content.&lt;/strong&gt; If you have a small piece of inline CSS that can't move to a stylesheet — a background colour from a database value, for example — CSP supports SHA-256 hashes as an alternative to nonces. You precompute the hash of the exact string and whitelist it. Surgical precision with no runtime overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explicitly exclude the &lt;code&gt;api&lt;/code&gt; group from the middleware.&lt;/strong&gt; Our implementation attaches the middleware to the &lt;code&gt;web&lt;/code&gt; group, which already keeps it off pure API routes. If your app registers any routes globally — or if you ever move to a flat route file — API responses will silently start receiving a CSP header they don't need. Being explicit with a group exclusion is safer than relying on registration order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Laravel 11 — bootstrap/app.php&lt;/span&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="kt"&gt;Middleware&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;web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;\App\Http\Middleware\SecureHeadersMiddleware&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="c1"&gt;// api group intentionally excluded — JSON responses don't need CSP&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After two weeks in report-only mode and one afternoon of allowlist tuning, we flipped to enforcement. Our security headers score went from D to A+ on &lt;a href="https://securityheaders.com" rel="noopener noreferrer"&gt;securityheaders.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since then, &lt;code&gt;csp-high-risk.log&lt;/code&gt; has caught two incidents where a third-party CDN attempted to inject tracking scripts we never authorised. The middleware caught what code review wouldn't have.&lt;/p&gt;

&lt;p&gt;If you want to configure your own policy without writing it from scratch, the &lt;a href="https://csp-generator.shakiltech.com" rel="noopener noreferrer"&gt;Laravel CSP Generator&lt;/a&gt; lets you do it visually — toggle your CDNs, integrations, and environment settings, and it outputs the full policy string and PHP middleware method ready to paste. The pre-enforcement checklist is built in and adapts to whatever you've configured.&lt;/p&gt;

&lt;p&gt;CSP isn't a silver bullet — it's one layer in a defence-in-depth approach alongside CSRF protection, input validation, prepared statements, and HTTPS. But unlike most defences, it actively stops an entire class of attacks at the browser level, rather than hoping your application-layer defences caught everything upstream.&lt;/p&gt;

&lt;p&gt;Start in report-only mode today. Watch your logs for a week. Then enforce. 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  Get the Quick Reference PDF
&lt;/h2&gt;

&lt;p&gt;If you want a single document to keep open while you're building — the full directive reference, the pre-enforcement checklist, common mistakes, and quick-start snippets — I put it all in a 4-page PDF.&lt;/p&gt;

&lt;p&gt;Drop your email below and it'll land in your inbox immediately. No series, no spam — just the PDF.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://csp-generator.shakiltech.com#guide" rel="noopener noreferrer"&gt;→ Get the Laravel CSP Quick Reference PDF&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;4 pages · All 13 directives · Pre-enforcement checklist · 5 common mistakes · Free&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does CSP break existing Laravel apps?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It can — specifically if you have inline scripts or styles without nonces. Use &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; first to surface everything that would be blocked, fix your templates, then switch to enforcement. This is the only safe migration path for an app that's already in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need CSP if I already have CSRF protection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. They protect against different attacks. CSRF prevents forged requests originating from other domains. CSP prevents injected scripts from &lt;em&gt;running on your domain&lt;/em&gt;. One does not substitute for the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need CSP if my app has no user-generated content?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. XSS doesn't require users to submit anything. A vulnerable dependency, a compromised CDN script, or a DOM-based XSS through a URL parameter are all real attack vectors that have nothing to do with user uploads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Vite HMR stopped working after I added CSP. What's wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two things to check. First, confirm &lt;code&gt;Vite::useCspNonce($nonce)&lt;/code&gt; is called before &lt;code&gt;$next($request)&lt;/code&gt;. Second, confirm &lt;code&gt;connect-src&lt;/code&gt; includes &lt;code&gt;ws://localhost:5173&lt;/code&gt; (or your Vite port) in your local environment. HMR uses a WebSocket and fails silently if that WebSocket connection is blocked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between &lt;code&gt;report-uri&lt;/code&gt; and &lt;code&gt;Report-To&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;report-uri&lt;/code&gt; is the original mechanism and is widely supported. &lt;code&gt;Report-To&lt;/code&gt; is the newer Reporting API — reports are batched, the endpoint URL is cached by the browser for months, and it covers more than just CSP. Use both: &lt;code&gt;report-uri&lt;/code&gt; as the universal fallback, &lt;code&gt;Report-To&lt;/code&gt; for modern browsers. Alternatively, skip rolling your own and use &lt;a href="https://report-uri.com" rel="noopener noreferrer"&gt;report-uri.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I debug CSP violations locally?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open DevTools → Console. Every CSP violation logs the exact blocked resource and the violated directive. That's your fastest debugging tool. If you're in report-only mode, the same violations also appear without anything actually breaking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle CSP with Livewire?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Livewire injects inline JavaScript that needs the nonce. For v2: &lt;code&gt;@livewireScripts(['nonce' =&amp;gt; $cspNonce])&lt;/code&gt;. For v3: &lt;code&gt;@livewireScriptConfig(['nonce' =&amp;gt; $cspNonce])&lt;/code&gt;. Both are shown in Step 3 above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about Google Analytics or Tag Manager?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;https://www.googletagmanager.com&lt;/code&gt; to &lt;code&gt;script-src&lt;/code&gt; and &lt;code&gt;https://www.google-analytics.com&lt;/code&gt; to both &lt;code&gt;script-src&lt;/code&gt; and &lt;code&gt;connect-src&lt;/code&gt;. GTM's custom HTML tags require &lt;code&gt;'unsafe-inline'&lt;/code&gt;, which weakens your policy — server-side GTM or direct GA4 integration are cleaner alternatives worth the migration cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Spatie's &lt;code&gt;laravel-csp&lt;/code&gt; package instead of a custom middleware?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Absolutely. &lt;a href="https://github.com/spatie/laravel-csp" rel="noopener noreferrer"&gt;Spatie's package&lt;/a&gt; provides a structured, per-policy-class approach with built-in nonce support and first-class test utilities. Our custom middleware made sense for us because we wanted every security header in one place without a package dependency. Either approach is valid — choose based on your team's preference for control versus convention.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you implemented CSP on a production Laravel app? Whether you had a smooth rollout, hit a rough edge with a third-party library, or are still sitting in report-only mode wondering what to do next — drop a comment. I'd love to hear where you got stuck, or what blocked resources surprised you most when you first flipped it on.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>security</category>
      <category>csp</category>
      <category>middleware</category>
    </item>
    <item>
      <title>Laravel External API Reliability: When Their System Goes Down</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Mon, 04 May 2026 08:47:07 +0000</pubDate>
      <link>https://dev.to/itxshakil/laravel-external-api-reliability-when-their-system-goes-down-2aj0</link>
      <guid>https://dev.to/itxshakil/laravel-external-api-reliability-when-their-system-goes-down-2aj0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Third-party APIs &lt;em&gt;will&lt;/em&gt; go down. This article walks through building a Laravel trait that makes your failure policy explicit per model (not buried in a catch block), adds a recovery path for failed syncs, and handles idempotency so retries don't create duplicate records. Skip to The Sync Trait if you're already familiar with the problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; Part 4 of 4 — Laravel Architecture Patterns for Production&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Reading time:&lt;/strong&gt; ~9 minutes&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Level:&lt;/strong&gt; Intermediate — some Laravel experience helps, but key concepts are explained inline&lt;/p&gt;



&lt;p&gt;Your code is correct. Your tests pass. Your application has been running fine for months.&lt;/p&gt;

&lt;p&gt;Then a third-party API goes down at 2am on a Tuesday.&lt;/p&gt;

&lt;p&gt;Registrations fail. Transaction syncs fail. Verification flows return 500s. Your queue fills with retry jobs. Users see errors they cannot act on. When the API comes back two hours later, you have a backlog of failed operations and no systematic way to recover them.&lt;/p&gt;

&lt;p&gt;None of this was a failure of your code. It was a failure of your &lt;em&gt;design&lt;/em&gt; to account for something that was always going to happen.&lt;/p&gt;

&lt;p&gt;The question is not how to prevent third-party outages — you cannot. The question is: when they happen, does your system &lt;strong&gt;behave&lt;/strong&gt;, or does it just &lt;strong&gt;fail&lt;/strong&gt;?&lt;/p&gt;


&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A trait that makes the fallback decision &lt;strong&gt;explicit in the model&lt;/strong&gt; — not buried in a catch block&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;fail-loud default&lt;/strong&gt; with a deliberate opt-in to fail-silent, per model&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;recovery pattern&lt;/strong&gt; so failed syncs become retryable, not permanent data loss&lt;/li&gt;
&lt;li&gt;An answer to the &lt;strong&gt;idempotency question&lt;/strong&gt; — what happens if the local write fails after the external API already succeeded?&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  The problem with try-catch at the call site
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;New to traits?&lt;/strong&gt; A &lt;a href="https://www.php.net/manual/en/language.oop5.traits.php" rel="noopener noreferrer"&gt;PHP trait&lt;/a&gt; is a reusable block of methods you can &lt;code&gt;use&lt;/code&gt; inside any class. Think of it as copy-pasting methods in, except the language handles it cleanly. In Laravel, traits are commonly added to Eloquent models to share behaviour across them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The instinctive approach to external API failures is to wrap the call in try-catch and handle it there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$externalApi&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attributes&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;Exception&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="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&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="c1"&gt;// Proceed anyway? Throw? Return false?&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 wrong — it is incomplete. The catch block becomes the moment you are forced to decide what the system should do when the API fails, under pressure, in the middle of writing unrelated code.&lt;/p&gt;

&lt;p&gt;That decision gets made &lt;strong&gt;inconsistently&lt;/strong&gt; across call sites, often defaults to "log and continue" because it is easiest, and is &lt;strong&gt;invisible&lt;/strong&gt; to anyone reading the model or service that defines the business rules.&lt;/p&gt;

&lt;p&gt;A better approach: make the fallback behaviour explicit at the model level, not the call site.&lt;/p&gt;




&lt;h2&gt;
  
  
  The sync trait: making resilience decisions visible
&lt;/h2&gt;

&lt;p&gt;The core idea is a trait on Eloquent models that wraps API calls and exposes the failure strategy as an overridable method.&lt;/p&gt;

&lt;p&gt;Create the file at &lt;code&gt;app/Traits/SyncsWithExternalApi.php&lt;/code&gt;. Every model that calls an external API will &lt;code&gt;use&lt;/code&gt; it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;New to Laravel's HTTP client?&lt;/strong&gt; Laravel ships with a wrapper around Guzzle called &lt;code&gt;Http::&lt;/code&gt;. It handles timeouts, headers, retries, and response checking in a clean, readable API. &lt;a href="https://laravel.com/docs/http-client" rel="noopener noreferrer"&gt;See the official docs →&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;trait&lt;/span&gt; &lt;span class="nc"&gt;SyncsWithExternalApi&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * The default: fail loud. Silence requires an explicit opt-in.
     *
     * This is deliberate — the safe default is to surface failures,
     * not hide them. Models that can tolerate temporary sync failures
     * should override this method and return true.
     */&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;shouldContinueWithoutSync&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Each model using this trait must implement createURL() and updateURL()
     * returning the external endpoint for that resource. For example:
     *
     * protected function createURL(): string
     * {
     *     return config('services.external_api.base_url') . '/users';
     * }
     */&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;createWithExternalApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&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;$url&lt;/span&gt; &lt;span class="o"&gt;=&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;static&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;createURL&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="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connectTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 3s to establish the TCP connection&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timeout&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="c1"&gt;// 30s maximum total request time&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withHeaders&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'X-Requested-With'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'XMLHttpRequest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'X-Forwarded-For'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;request&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;ip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'X-Request-ID'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Context&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;'request_id'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// see note below&lt;/span&gt;
            &lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$attributes&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;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;logger&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'External API failed on create'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'url'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$url&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="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'response'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;json&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;static&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;shouldContinueWithoutSync&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Explicit opt-in to fail-silent: proceed with local write only&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'External API unavailable.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Note: this only checks the HTTP status code, not the response body.&lt;/span&gt;
        &lt;span class="c1"&gt;// Some APIs return 200 OK with {"success": false} — add body validation&lt;/span&gt;
        &lt;span class="c1"&gt;// here based on what your specific API returns. See "What this does not solve".&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attributes&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Laravel version note:&lt;/strong&gt; &lt;code&gt;Context::get('request_id')&lt;/code&gt; requires &lt;strong&gt;Laravel 11&lt;/strong&gt; (&lt;code&gt;Illuminate\Support\Facades\Context&lt;/code&gt;). On Laravel 9 or 10, replace with: &lt;code&gt;request()-&amp;gt;attributes-&amp;gt;get(RequestLogger::REQUEST_ID_ATTRIBUTE, '')&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Why two timeout values?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connectTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timeout&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These cover two separate failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;connectTimeout(3)&lt;/code&gt;&lt;/strong&gt; — aborts if the TCP handshake takes more than 3 seconds. Catches unresponsive hosts before any data is sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;timeout(30)&lt;/code&gt;&lt;/strong&gt; — aborts if the total request takes more than 30 seconds. Catches a server that accepted the connection and then stalled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without either, a request to an unresponsive host may wait &lt;strong&gt;75+ seconds&lt;/strong&gt; for the OS to give up — blocking your worker the entire time. Setting both is cheap insurance.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On SSL verification:&lt;/strong&gt; You may see &lt;code&gt;withoutVerifying()&lt;/code&gt; in some codebases. This disables SSL certificate checking and is only appropriate for internal private-network services. For any public external API, leave SSL verification on. It costs nothing and catches real problems: expired certificates, man-in-the-middle attacks, DNS hijacking.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Tracing failures across systems with a request ID
&lt;/h2&gt;

&lt;p&gt;Notice the &lt;code&gt;X-Request-ID&lt;/code&gt; header on every outgoing call. This single line is worth a brief aside.&lt;/p&gt;

&lt;p&gt;When a sync fails and you are debugging with the third-party team, "can you look up request ID &lt;code&gt;a3f2c1...&lt;/code&gt; on your end?" takes seconds. "It was a POST to your /users endpoint, around 14:30, the payload had these fields..." takes several minutes and depends on their log retention.&lt;/p&gt;

&lt;p&gt;When both systems log the same ID, the conversation is immediate instead of archaeological.&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;withHeaders&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'X-Request-ID'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Context&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;'request_id'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One header. Costs nothing in normal operation. &lt;a href="https://microservices.io/patterns/observability/distributed-tracing.html" rel="noopener noreferrer"&gt;Correlation IDs are a standard observability pattern&lt;/a&gt; — if you are not already using them, this is a good place to start.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;shouldContinueWithoutSync()&lt;/code&gt; — a business decision in code
&lt;/h2&gt;

&lt;p&gt;This method is the most important design element in the trait. It is not a technical setting — it encodes a business rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If this integration fails, is the primary operation still valid without it?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The name matters. &lt;code&gt;ignoreException&lt;/code&gt; sounds like you are suppressing an error. &lt;code&gt;shouldContinueWithoutSync&lt;/code&gt; says exactly what it answers: can this model exist locally before the external system knows about it?&lt;/p&gt;

&lt;p&gt;Consider two models using the same trait with opposite answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// KYC verification — the external check IS the operation.&lt;/span&gt;
&lt;span class="c1"&gt;// If the API fails, the verification has not happened.&lt;/span&gt;
&lt;span class="c1"&gt;// Proceeding silently would mean skipping a compliance step.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ekyc&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&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;SyncsWithExternalApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// No override — shouldContinueWithoutSync() defaults to false.&lt;/span&gt;
    &lt;span class="c1"&gt;// A failed API call is a failed operation. Full stop.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stock transfer — the external system is a secondary record-keeper.&lt;/span&gt;
&lt;span class="c1"&gt;// Your database is the source of truth.&lt;/span&gt;
&lt;span class="c1"&gt;// A temporary outage is acceptable if you retry the sync later.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StockTransfer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&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;SyncsWithExternalApi&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;shouldContinueWithoutSync&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Explicit opt-in to fail-silent&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;Same trait. Opposite behaviour. The difference is &lt;strong&gt;documented in the code&lt;/strong&gt; — not buried in a service class comment somewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why default to &lt;code&gt;false&lt;/code&gt;?&lt;/strong&gt; Fail-loud surfaces problems. Fail-silent hides them. Defaulting to silence means every model that has not thought about this will eat errors quietly. Defaulting to loud means a developer has to explicitly say "yes, I have thought about this, and silent failure is acceptable here."&lt;/p&gt;




&lt;h3&gt;
  
  
  What a complete model looks like
&lt;/h3&gt;

&lt;p&gt;The trait expects each model to implement three methods alongside the policy setting: &lt;code&gt;createURL()&lt;/code&gt;, &lt;code&gt;updateURL()&lt;/code&gt;, and optionally &lt;code&gt;requiredAttributes()&lt;/code&gt;. Here is &lt;code&gt;StockTransfer&lt;/code&gt; with all the pieces assembled:&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;StockTransfer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&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;SyncsWithExternalApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Policy: can exist locally before the external system knows.&lt;/span&gt;
    &lt;span class="c1"&gt;// Failed syncs queue a retry rather than failing the operation.&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;shouldContinueWithoutSync&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Where to create a new transfer in the external system&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;createURL&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="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.core_banking.url'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/transfers'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Where to update an existing transfer.&lt;/span&gt;
    &lt;span class="c1"&gt;// Return an array if multiple endpoints need to be notified.&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;updateURL&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="k"&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="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.core_banking.url'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/transfers/'&lt;/span&gt; &lt;span class="mf"&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;external_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Fields required in EVERY update payload — not just what changed.&lt;/span&gt;
    &lt;span class="c1"&gt;// The external system uses these to locate the record on their end.&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;requiredAttributes&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;'external_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;external_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'account_ref'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;external_ref&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;The contract the trait expects: &lt;code&gt;createURL()&lt;/code&gt; and &lt;code&gt;updateURL()&lt;/code&gt; say where to send data. &lt;code&gt;requiredAttributes()&lt;/code&gt; says what the external system always needs to receive alongside any change. &lt;code&gt;shouldContinueWithoutSync()&lt;/code&gt; says what happens when the endpoint is unreachable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling updates across multiple endpoints
&lt;/h2&gt;

&lt;p&gt;Some models need to notify more than one external endpoint on update. The trait handles this by normalising the URL list:&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;updateDetailWithExternalApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;?bool&lt;/span&gt; &lt;span class="nv"&gt;$shouldContinueWithoutSync&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="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$urls&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;updateURL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Accept a single string or an array — both work the same way&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// requiredAttributes() injects fields that must always accompany updates,&lt;/span&gt;
        &lt;span class="c1"&gt;// so call sites don't have to remember to include them&lt;/span&gt;
        &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_merge&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;requiredAttributes&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;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connectTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timeout&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withHeaders&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'X-Requested-With'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'XMLHttpRequest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'X-Forwarded-For'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;request&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;ip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'X-Request-ID'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Context&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;'request_id'&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$payload&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;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;failed&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;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;logger&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'External sync failed on update'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'url'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$url&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="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'data'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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="nv"&gt;$proceed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$shouldContinueWithoutSync&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;shouldContinueWithoutSync&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;$proceed&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Sync failed for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: "&lt;/span&gt; &lt;span class="mf"&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;status&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why the &lt;code&gt;?bool $shouldContinueWithoutSync = null&lt;/code&gt; parameter?&lt;/strong&gt; The model-level method sets the default. But callers sometimes need to override it for a specific context — a reconciliation job, for instance, might set &lt;code&gt;shouldContinueWithoutSync: true&lt;/code&gt; on models that normally fail loud, because the job's purpose is to retry failures and hard-failing the whole batch over one record defeats that purpose. The parameter gives callers that flexibility without touching the model.&lt;/p&gt;




&lt;h2&gt;
  
  
  What happens to failed syncs
&lt;/h2&gt;

&lt;p&gt;The trait handles the &lt;em&gt;moment&lt;/em&gt; of failure. Recovery is a separate concern — deliberately so.&lt;/p&gt;

&lt;p&gt;When a model creates locally because the API was down, you have data your external system does not know about. Eventually this needs to reconcile. Two complementary approaches:&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 1: Queued delayed retry (start here)
&lt;/h3&gt;

&lt;p&gt;When a model proceeds locally because the API was down, queue a retry job immediately — but with a delay. The delay matters: if the API just went down, hitting it again in five seconds is pointless.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is a job class?&lt;/strong&gt; In Laravel, a &lt;a href="https://laravel.com/docs/queues#creating-jobs" rel="noopener noreferrer"&gt;job&lt;/a&gt; is a class that does background work. Create one with &lt;code&gt;php artisan make:job SyncRecordWithExternalApi&lt;/code&gt;. It receives the record ID, loads the model, and calls the external API. The queue worker picks it up and runs it outside the request cycle.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Update the fail-silent branch in your trait to dispatch the retry alongside the local write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;static&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;shouldContinueWithoutSync&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Do not retry immediately — the API just failed.&lt;/span&gt;
    &lt;span class="c1"&gt;// Wait 5 minutes, then attempt the sync again.&lt;/span&gt;
    &lt;span class="c1"&gt;// SyncRecordWithExternalApi is a job class you create:&lt;/span&gt;
    &lt;span class="c1"&gt;// php artisan make:job SyncRecordWithExternalApi&lt;/span&gt;
    &lt;span class="nc"&gt;SyncRecordWithExternalApi&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;$record&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="nf"&gt;onQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sync'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;delay&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;addMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$record&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;New to queues?&lt;/strong&gt; Laravel's queue system defers work to a background worker process. &lt;a href="https://laravel.com/docs/queues" rel="noopener noreferrer"&gt;See the official docs →&lt;/a&gt;. For jobs that communicate with external APIs, use a dedicated queue (&lt;code&gt;.onQueue('sync')&lt;/code&gt;) so a third-party outage does not back up your other work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Approach 2: Scheduled reconciliation (add after you've seen one outage)
&lt;/h3&gt;

&lt;p&gt;A nightly job that checks for any records that were never synced — a safety net for anything that slipped through after retries were exhausted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Runs nightly via Laravel's scheduler&lt;/span&gt;
&lt;span class="nv"&gt;$unsynced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whereNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'synced_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;cursor&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;$unsynced&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SyncRecordWithExternalApi&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;$record&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="nf"&gt;onQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sync'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Add a &lt;code&gt;synced_at&lt;/code&gt; column to any model where &lt;code&gt;shouldContinueWithoutSync()&lt;/code&gt; returns true. Set it when the sync succeeds. That gives you an instant view of unsynced records and makes the reconciliation job trivial to write.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Start with the delayed retry.&lt;/strong&gt; Add the scheduled reconciliation after you have seen your first real outage and understand your actual failure volume. Both together is more resilient than either alone.&lt;/p&gt;

&lt;p&gt;Choosing neither — logging the error and moving on — means the inconsistency is permanent unless someone notices and manually resolves it. The cost of building the recovery path is small. The cost of skipping it compounds silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The edge case: what if the local write fails?
&lt;/h2&gt;

&lt;p&gt;Show this trait to a more experienced engineer and the first question is usually:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What happens if the local &lt;code&gt;create()&lt;/code&gt; fails after the external API already succeeded?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The API has the record. Your database does not. You have introduced inconsistency in the other direction.&lt;/p&gt;

&lt;p&gt;The complete solution requires two things working together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrap the local write in a database transaction&lt;/strong&gt; so it can roll back cleanly on failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send an idempotency key&lt;/strong&gt; with the external API call, so a retry does not create a duplicate on their end.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is an idempotency key?&lt;/strong&gt; It is a unique identifier you generate before making an API call and send as a header. If the request fails and you retry with the same key, the external API recognises it as a repeat and returns the original result — without creating a duplicate record. &lt;a href="https://stripe.com/docs/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe's idempotency docs&lt;/a&gt; are a good reference for how this works in practice.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&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;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;createWithExternalApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&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;$url&lt;/span&gt; &lt;span class="o"&gt;=&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;static&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;createURL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Generate the key before the call.&lt;/span&gt;
    &lt;span class="c1"&gt;// On retry, send the same key — the external API returns&lt;/span&gt;
    &lt;span class="c1"&gt;// the same result without creating a duplicate.&lt;/span&gt;
    &lt;span class="nv"&gt;$idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="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="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connectTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timeout&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withHeaders&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'X-Idempotency-Key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'X-Request-ID'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Context&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;'request_id'&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$attributes&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;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// API failed before writing — safe to retry with the same key&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'External API unavailable.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// API succeeded. Wrap the local write in a transaction so it can&lt;/span&gt;
    &lt;span class="c1"&gt;// roll back cleanly if something goes wrong (e.g. a validation error&lt;/span&gt;
    &lt;span class="c1"&gt;// or a database constraint). If the transaction fails, retrying the&lt;/span&gt;
    &lt;span class="c1"&gt;// whole operation with the same idempotency key is safe — the API&lt;/span&gt;
    &lt;span class="c1"&gt;// won't create a duplicate on the next attempt.&lt;/span&gt;
    &lt;span class="k"&gt;return&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;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attributes&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Check your API's documentation.&lt;/strong&gt; Not all external APIs support idempotency keys — it depends on the provider. When they are available, use them. Without them, the only safe option after a partial failure is manual reconciliation, and manual steps under incident pressure go wrong.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What this does not solve
&lt;/h2&gt;

&lt;p&gt;A pattern is a floor, not a ceiling. Be clear about its limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200 responses with error payloads.&lt;/strong&gt; &lt;code&gt;$response-&amp;gt;failed()&lt;/code&gt; checks the HTTP status code. Some APIs return &lt;code&gt;200 OK&lt;/code&gt; with &lt;code&gt;{"success": false}&lt;/code&gt; in the body. The update method above checks for this, but the create method does not — add body validation based on what your specific API returns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent data corruption.&lt;/strong&gt; A successful response does not guarantee the data was stored correctly. For critical operations, validate the response body against what you sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial writes on the external end.&lt;/strong&gt; Some APIs accept a request, return success, and write only part of the data. Only end-to-end testing and periodic reconciliation catches this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Providers that don't support idempotency keys.&lt;/strong&gt; Nothing in this pattern solves the duplicate-create problem if the external API does not support them.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where you end up
&lt;/h2&gt;

&lt;p&gt;If you apply this consistently, one thing changes: the fallback decision is not made under pressure at 2am. It was made when the model was written, by someone who understood the business rule, and it is readable in the code by anyone who comes after them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;shouldContinueWithoutSync()&lt;/code&gt; on each model. Logged context on every failure. A recovery path built in, not improvised.&lt;/p&gt;

&lt;p&gt;The 2am outage will still happen. This just means you will have a plan when it does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you ship — checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every &lt;code&gt;Http::&lt;/code&gt; call has both &lt;code&gt;connectTimeout()&lt;/code&gt; and &lt;code&gt;timeout()&lt;/code&gt; set explicitly&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;withoutVerifying()&lt;/code&gt; is not present on any call to a public external API&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;shouldContinueWithoutSync()&lt;/code&gt; return value is a &lt;strong&gt;conscious decision&lt;/strong&gt; per model — not left as the default by accident&lt;/li&gt;
&lt;li&gt;[ ] Models where &lt;code&gt;shouldContinueWithoutSync()&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; have a &lt;code&gt;synced_at&lt;/code&gt; column and a recovery path (queued retry, scheduled reconciliation, or both)&lt;/li&gt;
&lt;li&gt;[ ] Every failed sync logs: URL, status code, response body, and the payload sent&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;X-Request-ID&lt;/code&gt; is passed as a header on all outgoing API calls&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;requiredAttributes()&lt;/code&gt; is defined on any model that must include identifying fields in every update&lt;/li&gt;
&lt;li&gt;[ ] Idempotency keys are used for any external API that supports them&lt;/li&gt;
&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; The fallback decision — fail loud or continue without sync — is a business rule, not a technical setting. Encoding it as a named method on the model (&lt;code&gt;shouldContinueWithoutSync()&lt;/code&gt;) makes the decision readable in the code, consistent across every call site, and impossible to forget to make.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Previous: &lt;a href="https://blog.shakiltech.com/laravel-secure-file-upload/" rel="noopener noreferrer"&gt;Part 3 — Secure File Uploads&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What this series built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part 1&lt;/strong&gt; turned "we don't know what changed" into a traceable system: field-level diffs, append-only logs, request IDs that correlate across every log channel, and permission failures visible before they become incidents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2&lt;/strong&gt; moved from "dispatch and hope" to intentional queue design: topology that separates work by characteristics, job constructors that carry IDs not snapshots, retry strategies matched to actual failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3&lt;/strong&gt; replaced a single file-type check with seven independent ones — each named, each closing a specific attack vector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4&lt;/strong&gt; made "what happens when their API goes down?" answerable before it becomes an incident.&lt;/p&gt;

&lt;p&gt;The common thread: decisions that are usually made implicitly — or not at all until something breaks — made explicit in code, with the reasoning attached.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://laravel.com/docs/http-client" rel="noopener noreferrer"&gt;Laravel HTTP Client&lt;/a&gt; — official docs for &lt;code&gt;Http::&lt;/code&gt;, including retry helpers and middleware&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://laravel.com/docs/queues" rel="noopener noreferrer"&gt;Laravel Queues&lt;/a&gt; — how to set up workers, delayed dispatch, and failure handling&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://laravel.com/docs/queues#creating-jobs" rel="noopener noreferrer"&gt;Laravel Job Classes&lt;/a&gt; — how to create the &lt;code&gt;SyncRecordWithExternalApi&lt;/code&gt; job used in the recovery pattern&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://stripe.com/docs/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe: Idempotent Requests&lt;/a&gt; — the clearest real-world explanation of idempotency keys&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://microservices.io/patterns/observability/distributed-tracing.html" rel="noopener noreferrer"&gt;Microservices: Distributed Tracing pattern&lt;/a&gt; — why correlation IDs matter across systems&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.honeybadger.io/for/php/" rel="noopener noreferrer"&gt;Honeybadger for PHP&lt;/a&gt; — exception monitoring that surfaces failed syncs before users report them&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>laravel</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Secure File Uploads: Seven Checks and Why Each One Exists</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Fri, 01 May 2026 13:26:57 +0000</pubDate>
      <link>https://dev.to/itxshakil/secure-file-uploads-seven-checks-and-why-each-one-exists-222n</link>
      <guid>https://dev.to/itxshakil/secure-file-uploads-seven-checks-and-why-each-one-exists-222n</guid>
      <description>&lt;p&gt;&lt;strong&gt;Part 3 of 4 — Laravel Architecture Patterns for Production&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;~9 min read · Security · Middleware · File handling&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;A file upload is the moment you hand control to an untrusted user.&lt;/p&gt;

&lt;p&gt;Everything else in your application — form inputs, query parameters, JSON — is text. You validate it, sanitize it, store it in a database. A file upload is arbitrary binary data from a source you cannot verify, about to be written to your filesystem. The surface area is different. The failure modes are different. The consequences are different.&lt;/p&gt;

&lt;p&gt;Most tutorials cover the happy path: accept the file, store it, return a URL. The problem is never the happy path.&lt;/p&gt;

&lt;p&gt;Rename a PHP file to &lt;code&gt;image.jpg&lt;/code&gt;. Upload it. Many Laravel applications accept it — because &lt;code&gt;getClientOriginalExtension()&lt;/code&gt; returns &lt;code&gt;jpg&lt;/code&gt; and &lt;code&gt;getClientMimeType()&lt;/code&gt; returns &lt;code&gt;image/jpeg&lt;/code&gt;. Both values the &lt;em&gt;client&lt;/em&gt; provided. Neither verified by the server.&lt;/p&gt;

&lt;p&gt;This is how webshells get uploaded. This is how stored XSS gets planted in SVG files. This is the gap.&lt;/p&gt;

&lt;p&gt;The fix is seven independent checks, each closing a specific attack vector. The reason for seven — not one — is that no single check catches everything. A determined attacker probes each layer individually. Defense in depth means removing the easy paths at each layer.&lt;/p&gt;


&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A middleware that validates seven independent properties of every uploaded file, each one named and explained&lt;/li&gt;
&lt;li&gt;Server-side MIME detection using &lt;code&gt;finfo&lt;/code&gt; — not the browser's claim&lt;/li&gt;
&lt;li&gt;A unique name strategy that removes user-controlled strings from your filesystem paths entirely&lt;/li&gt;
&lt;li&gt;A storage pattern where no uploaded file is ever directly reachable via HTTP&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  The validation pipeline
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload arrives
       │
       ▼
  1. Filename sanitization
     blocks: path traversal · double extensions · null bytes · hidden files
       │
       ▼
  2. Extension allowlist
     blocks: file types with no legitimate use case
       │
       ▼
  3. Client MIME allowlist
     blocks: mismatched browser-reported content type
       │
       ▼
  4. Server MIME detection (finfo reads actual bytes)
     blocks: renamed files · spoofed extensions
       │
       ▼
  5. Extension ↔ MIME cross-check
     blocks: shell.php.jpg · mismatched pairs
       │
       ▼
  6. File size limit
     blocks: denial-of-service via oversized uploads
       │
       ▼
  7. Magic bytes check
     blocks: crafted files · polyglot attacks
       │
       ▼
  Stored as unique name in storage/ — never in public/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The middleware
&lt;/h2&gt;

&lt;p&gt;The validation runs as middleware applied to upload routes. All validation aborts with &lt;code&gt;400&lt;/code&gt; and a user-readable message:&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;SecureFileUpload&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only what your application genuinely needs — keep this narrow&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$allowedExtensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'jpeg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'png'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'docx'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'xlsx'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'csv'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$allowedMimeTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'image/jpeg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'image/png'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'application/pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'application/vnd.openxmlformats-officedocument.wordprocessingml.document'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'text/csv'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'text/plain'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'application/vnd.ms-excel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Maps each allowed extension to its permitted server-detected MIME types&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$extensionMimeMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'jpg'&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;'image/jpeg'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'jpeg'&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;'image/jpeg'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'png'&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;'image/png'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'pdf'&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;'application/pdf'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'docx'&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;'application/vnd.openxmlformats-officedocument.wordprocessingml.document'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'xlsx'&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;'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'csv'&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;'text/csv'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'text/plain'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'application/vnd.ms-excel'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$maxFileSizeKb&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="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;maxFileSizeKb&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="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'files.upload_max_kb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5120&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Default 5MB&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="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;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allFiles&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;$file&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;walkFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&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="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;walkFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&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="nb"&gt;is_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$f&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;walkFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$f&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;$file&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;UploadedFile&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;validateFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Orchestrates all seven checks in order&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;validateFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UploadedFile&lt;/span&gt; &lt;span class="nv"&gt;$file&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;$rawName&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getClientOriginalName&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;$file&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="nv"&gt;$clientMime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getClientMimeType&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Check 1 — filename sanitization&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;sanitizeRawName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rawName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Check 2 — extension allowlist&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;allowedExtensions&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="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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Files with .&lt;/span&gt;&lt;span class="nv"&gt;$extension&lt;/span&gt;&lt;span class="s2"&gt; extension are not supported."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Check 3 — client MIME allowlist&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$clientMime&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;allowedMimeTypes&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="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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Unrecognized file type. Ensure the file is not corrupted.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Check 4 — server-side MIME detection&lt;/span&gt;
        &lt;span class="nv"&gt;$serverMime&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;detectServerMime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Check 5 — extension ↔ MIME cross-check&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isMimeAllowedForExtension&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="nv"&gt;$serverMime&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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Extension '.&lt;/span&gt;&lt;span class="nv"&gt;$extension&lt;/span&gt;&lt;span class="s2"&gt;' does not match the file's content. "&lt;/span&gt;
                      &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"Please ensure you have not renamed a file to a different extension."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Check 6 — size limit&lt;/span&gt;
        &lt;span class="nv"&gt;$sizeKb&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="nb"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&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;$sizeKb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;maxFileSizeKb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$maxMb&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;maxFileSizeKb&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&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;$fileMb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sizeKb&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&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="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"File too large. Maximum is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$maxMb&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;MB. Your file is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$fileMb&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;MB."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Check 7 — magic bytes&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;lightweightMagicChecks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&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="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;isMimeAllowedForExtension&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="nv"&gt;$serverMime&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$allowed&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;extensionMimeMap&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="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serverMime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowed&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="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 error messages matter. "Invalid file type" forces a support ticket. "Only one dot is allowed in the filename (e.g. &lt;code&gt;document.pdf&lt;/code&gt;)" tells the user exactly what is wrong. When legitimate users trigger these checks by accident — and they will — specificity is the difference between a solvable problem and frustrated churn.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 1 — Filename sanitization
&lt;/h2&gt;

&lt;p&gt;Before any content check, validate the name itself. Filenames are a distinct attack surface:&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;sanitizeRawName&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="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="nb"&gt;mb_strlen&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;'UTF-8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Filename too long. Please use 100 characters or fewer.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Path traversal: ../../etc/passwd&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&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="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="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Filename cannot contain path separators.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Null bytes and control characters&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[\x00-\x1F\x7F]/'&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="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Filename contains invalid characters. Please rename the file.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Double-dot path traversal&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str_contains&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;'..'&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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Filename cannot contain '..'."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Double-extension attack: shell.php.jpg&lt;/span&gt;
    &lt;span class="c1"&gt;// The browser sees .jpg, the server may execute .php&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;substr_count&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;'.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;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;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Only one dot allowed in the filename (e.g. 'document.pdf')."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Hidden files: .htaccess, .env&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&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;$name&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="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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Filenames cannot start with a dot.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Strict allowlist: letters, numbers, underscores, dashes, one dot, spaces&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^[a-zA-Z0-9_\-\. ]+$/'&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="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Filename contains unsupported characters. Use letters, numbers, dashes, and underscores.'&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;$name&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 double-extension rule is the highest-value check here. &lt;code&gt;shell.php.jpg&lt;/code&gt; gets past a simple extension check because the last extension is &lt;code&gt;.jpg&lt;/code&gt;. One dot, full stop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checks 2 and 3 — Extension and client MIME allowlisting
&lt;/h2&gt;

&lt;p&gt;Both are necessary, even though neither is sufficient alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;allowedExtensions&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="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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Files with .&lt;/span&gt;&lt;span class="nv"&gt;$extension&lt;/span&gt;&lt;span class="s2"&gt; extension are not supported."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$clientMime&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;allowedMimeTypes&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="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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Unrecognized file type. Ensure the file is not corrupted.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why both?&lt;/strong&gt; An attacker who crafts a file with an allowed extension might supply an unexpected MIME type. An attacker who spoofs the MIME type might use a disallowed extension. Checking both raises the bar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why keep the allowlist narrow?&lt;/strong&gt; Every extension you permit is an attack surface you are accepting. If your application does not need &lt;code&gt;.svg&lt;/code&gt; uploads, remove it. If it does not need &lt;code&gt;.zip&lt;/code&gt;, remove it. The default should be minimal; expansion should require a specific business case.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 4 — Server-side MIME detection
&lt;/h2&gt;

&lt;p&gt;This is the check that actually matters. The previous two trusted the client. This one reads the file:&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;detectServerMime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UploadedFile&lt;/span&gt; &lt;span class="nv"&gt;$file&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="c1"&gt;// finfo reads the file's actual bytes from the filesystem.&lt;/span&gt;
    &lt;span class="c1"&gt;// It does not care what the browser claimed.&lt;/span&gt;
    &lt;span class="c1"&gt;// A PHP file renamed to .jpg will be identified as text/x-php, not image/jpeg.&lt;/span&gt;
    &lt;span class="nv"&gt;$finfo&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;\finfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;FILEINFO_MIME_TYPE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$mime&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$finfo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&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="c1"&gt;// Edge case: finfo correctly identifies CSV as text/plain (it is plain text).&lt;/span&gt;
    &lt;span class="c1"&gt;// But text/plain is not in the CSV slot on the allowlist.&lt;/span&gt;
    &lt;span class="c1"&gt;// The extension provides the context needed to resolve this.&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;$mime&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'text/plain'&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;$file&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;'csv'&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="s1"&gt;'text/csv'&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="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$mime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why does the CSV edge case matter?&lt;/strong&gt; A naive implementation would either reject all CSVs (wrong MIME type) or add &lt;code&gt;text/plain&lt;/code&gt; to the global allowlist (which permits uploading arbitrary text files, including PHP in some environments). The explicit edge case handles the ambiguity without either problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 5 — Cross-checking extension against detected MIME
&lt;/h2&gt;

&lt;p&gt;The check that closes the most common attack vector. The &lt;code&gt;extensionMimeMap&lt;/code&gt; defined on the class is what makes this work — each allowed extension maps to the MIME types &lt;code&gt;finfo&lt;/code&gt; legitimately returns for that format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CSV legitimately maps to multiple MIME types because different tools&lt;/span&gt;
&lt;span class="c1"&gt;// and operating systems report it differently. The map accommodates&lt;/span&gt;
&lt;span class="c1"&gt;// known variation without widening the gap for other formats.&lt;/span&gt;
&lt;span class="s1"&gt;'csv'&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;'text/csv'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'text/plain'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'application/vnd.ms-excel'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A file with extension &lt;code&gt;.jpg&lt;/code&gt; whose &lt;code&gt;finfo&lt;/code&gt;-detected MIME is &lt;code&gt;text/x-php&lt;/code&gt; fails this check. The extension says one thing; the bytes say another. That inconsistency is the signal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 6 — Size limits
&lt;/h2&gt;

&lt;p&gt;The size check in &lt;code&gt;validateFile()&lt;/code&gt; above reads the limit from config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;maxFileSizeKb&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="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'files.upload_max_kb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5120&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the limit in &lt;code&gt;config('files.upload_max_kb')&lt;/code&gt; rather than hardcoding it. This lets you adjust per environment — tighter on free tier plans, looser for premium users — without touching the middleware. The error message surfaces both the maximum and the actual file size so the user knows exactly what to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 7 — Magic bytes
&lt;/h2&gt;

&lt;p&gt;The final layer reads the file's opening bytes directly:&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;lightweightMagicChecks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UploadedFile&lt;/span&gt; &lt;span class="nv"&gt;$file&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;$ext&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;$fp&lt;/span&gt;   &lt;span class="o"&gt;=&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;$file&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;$head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;fread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;@&lt;/span&gt;&lt;span class="nb"&gt;fclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Every valid PDF begins with %PDF-&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;$ext&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'pdf'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;str_starts_with&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;bytesToAscii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$head&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'%PDF-'&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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'This PDF appears to be corrupted or invalid.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// PNG has a fixed 8-byte signature defined in the spec&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;$ext&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'png'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$head&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x89&lt;/span&gt;&lt;span class="s2"&gt;PNG&lt;/span&gt;&lt;span class="se"&gt;\r\n\x1a\n&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="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'This PNG appears to be corrupted or invalid.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// JPEG starts with FF D8 FF&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'jpeg'&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="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;$head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\xFF\xD8\xFF&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="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'This JPEG appears to be corrupted or invalid.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// DOCX, XLSX, PPTX are ZIP archives with a PK header&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'docx'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'xlsx'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pptx'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'zip'&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^PK(\x03\x04|\x05\x06|\x07\x08)/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$head&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="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'This Office document appears corrupted or invalid.'&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;bytesToAscii&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;$bytes&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="c1"&gt;// Strip non-printable characters for string comparison (e.g. "%PDF-")&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[^\x20-\x7E]/'&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="nv"&gt;$bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why 8 bytes?&lt;/strong&gt; Magic byte signatures are typically 2–8 bytes. Reading the first 8 captures all common signatures without reading a meaningful chunk of the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on coverage:&lt;/strong&gt; this check covers the formats most commonly exploited — PDF, PNG, JPEG, and Office documents. It is a structural sanity check, not a full file parser. Its job is to catch files where the opening bytes contradict the claimed format. It does not replace the MIME detection and cross-check above — all seven checks work together.&lt;/p&gt;




&lt;h2&gt;
  
  
  SVG: the format that deserves extra attention
&lt;/h2&gt;

&lt;p&gt;SVG files are XML. XML can contain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags. A valid, well-formed SVG with embedded JavaScript is a stored XSS vector — if you accept it from one user and serve it inline to another, you have created a script injection path. The &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP File Upload Cheat Sheet&lt;/a&gt; covers this and other format-specific risks in depth.&lt;/p&gt;

&lt;p&gt;Three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sanitize on upload&lt;/strong&gt; — strip script tags and event handlers before storing. Complex to do correctly and easy to get wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force download&lt;/strong&gt; — serve SVGs with &lt;code&gt;Content-Disposition: attachment&lt;/code&gt;, preventing inline browser rendering and therefore script execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove SVGs from the allowlist&lt;/strong&gt; — the safest option if there is no specific business requirement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you do not have a defined reason to accept user-uploaded SVGs, option 3 takes one line and eliminates the risk entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Always generate a unique name before saving
&lt;/h2&gt;

&lt;p&gt;Regardless of the validation outcome, the filename used for storage should never be the one the user provided:&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;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$postfix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;UploadedFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getClientOriginalName&lt;/span&gt;&lt;span class="p"&gt;();&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="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$nameWithoutExtension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&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;replaceLast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;".&lt;/span&gt;&lt;span class="nv"&gt;$extension&lt;/span&gt;&lt;span class="s2"&gt;"&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;replace&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="s1"&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;$nameWithoutExtension&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$nameWithoutExtension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$nameWithoutExtension&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;substr&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="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$nameWithoutExtension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$nameWithoutExtension&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$postfix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;method_exists&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="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'transliterate'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$nameWithoutExtension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[^\x20-\x7E]/'&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="nv"&gt;$nameWithoutExtension&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;$nameWithoutExtension&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;uniqid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="mf"&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="k"&gt;return&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;transliterate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$nameWithoutExtension&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;uniqid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="mf"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;uniqid()&lt;/code&gt; is microsecond-based and not cryptographically random. It is sufficient here because it combines with the sanitized filename prefix — two simultaneous uploads of &lt;code&gt;report.pdf&lt;/code&gt; produce &lt;code&gt;report_6601a1f3b8e42.pdf&lt;/code&gt; and &lt;code&gt;report_6601a1f3b8e43.pdf&lt;/code&gt;, with no collision. If your application requires stronger uniqueness guarantees — for example, if filenames are ever used as access tokens — substitute &lt;code&gt;Str::uuid()&lt;/code&gt;. That is a one-line change and the stronger default.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;uniqueName()&lt;/code&gt; depends on two helper methods. Here are simple implementations to drop into the same &lt;code&gt;File&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="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;getExtension&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="nc"&gt;UploadedFile&lt;/span&gt; &lt;span class="nv"&gt;$file&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;UploadedFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;pathinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PATHINFO_EXTENSION&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;getHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UploadedFile&lt;/span&gt; &lt;span class="nv"&gt;$file&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="c1"&gt;// SHA-256 of the file contents — useful for detecting duplicate uploads&lt;/span&gt;
    &lt;span class="c1"&gt;// and verifying file integrity when serving back to users&lt;/span&gt;
    &lt;span class="k"&gt;return&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;$file&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the media trait:&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;addMediaAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;UploadedFile&lt;/span&gt; &lt;span class="nv"&gt;$file&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;$collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'default'&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="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="kt"&gt;Media&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$uniqueName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;medias&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'name'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getClientOriginalName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// Original name for display&lt;/span&gt;
        &lt;span class="s1"&gt;'file_name'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$uniqueName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// Unique name for storage&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;$file&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;'path'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;storeAs&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;getMediaPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                            &lt;span class="nv"&gt;$uniqueName&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;getMediaDisk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="p"&gt;)&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMediaDisk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'file_hash'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'collection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'size'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$file&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="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why keep the original name in &lt;code&gt;name&lt;/code&gt; but use the unique name for &lt;code&gt;path&lt;/code&gt;?&lt;/strong&gt; The display name — shown to the user in the UI — should be what they uploaded. The storage path should have no connection to any user-provided string. The two concerns are separate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where files live after validation
&lt;/h2&gt;

&lt;p&gt;Store outside the web root — in &lt;code&gt;storage/&lt;/code&gt; — and serve through a controller that enforces authorization:&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;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Media&lt;/span&gt; &lt;span class="nv"&gt;$media&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;StreamedResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Authorization on every file access — no direct URL bypasses this&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;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'view'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$media&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;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;$media&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;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;$media&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$media&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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$media&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mime_type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why never &lt;code&gt;public/&lt;/code&gt;?&lt;/strong&gt; A file in &lt;code&gt;public/&lt;/code&gt; is directly reachable via HTTP — no authorization, no application code in the path. If a file with a server-executable extension reaches &lt;code&gt;public/&lt;/code&gt; because validation has layers that can be circumvented, it can be executed by guessing its URL. Files in &lt;code&gt;storage/&lt;/code&gt; have no direct URL. Everything goes through the controller.&lt;/p&gt;




&lt;h2&gt;
  
  
  Registering the middleware
&lt;/h2&gt;

&lt;p&gt;Apply &lt;code&gt;SecureFileUpload&lt;/code&gt; to upload routes only — never globally, since running all seven checks on every request that has no file would add overhead for no reason.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Laravel 11 — routes/web.php or routes/api.php&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;'/upload'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;MediaController&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;'store'&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;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\App\Http\Middleware\SecureFileUpload&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="c1"&gt;// Or as a group if you have multiple upload endpoints&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;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\App\Http\Middleware\SecureFileUpload&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;group&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;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;'/media/upload'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;MediaController&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;'store'&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;'/documents/upload'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DocumentController&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;'store'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Laravel 10 and earlier — same route syntax works, or alias it&lt;/span&gt;
&lt;span class="c1"&gt;// in app/Http/Kernel.php under $routeMiddleware:&lt;/span&gt;
&lt;span class="c1"&gt;// 'secure.upload' =&amp;gt; \App\Http\Middleware\SecureFileUpload::class,&lt;/span&gt;
&lt;span class="c1"&gt;// Then use -&amp;gt;middleware('secure.upload') in your routes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The key insight from this article:&lt;/strong&gt; No single file validation check catches everything. &lt;code&gt;getClientMimeType()&lt;/code&gt; trusts the browser. &lt;code&gt;getClientOriginalExtension()&lt;/code&gt; trusts the browser. Only &lt;code&gt;finfo&lt;/code&gt; reads the actual bytes. Seven independent checks layered together — each blocking a different attack path — is what makes upload validation meaningfully secure rather than just present.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before you ship — checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;SecureFileUpload&lt;/code&gt; middleware is applied to upload routes only — not globally&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;allowedExtensions&lt;/code&gt; and &lt;code&gt;allowedMimeTypes&lt;/code&gt; are minimal — only what the application genuinely needs&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;finfo&lt;/code&gt; server-side MIME detection is running (not just trusting &lt;code&gt;getClientMimeType()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;extensionMimeMap&lt;/code&gt; covers every extension in &lt;code&gt;allowedExtensions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Magic bytes check covers JPEG in addition to PDF, PNG, and Office formats&lt;/li&gt;
&lt;li&gt;[ ] SVG is either removed from the allowlist or served with &lt;code&gt;Content-Disposition: attachment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;File::uniqueName()&lt;/code&gt; is called before every &lt;code&gt;storeAs()&lt;/code&gt; — original filename never used as storage path&lt;/li&gt;
&lt;li&gt;[ ] Files are stored in &lt;code&gt;storage/&lt;/code&gt; — nothing uploaded goes into &lt;code&gt;public/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] File access goes through a controller that calls &lt;code&gt;$this-&amp;gt;authorize()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Previous: &lt;a href="https://blog.shakiltech.com/laravel-queue-architecture/" rel="noopener noreferrer"&gt;Part 2 — Queue Architecture&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>laravel</category>
      <category>php</category>
      <category>security</category>
    </item>
    <item>
      <title>Laravel Queue Architecture: Designing Background Work That Holds Up</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Fri, 24 Apr 2026 17:21:11 +0000</pubDate>
      <link>https://dev.to/itxshakil/laravel-queue-architecture-designing-background-work-that-holds-up-h6e</link>
      <guid>https://dev.to/itxshakil/laravel-queue-architecture-designing-background-work-that-holds-up-h6e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Part 2 of 4 — Laravel Architecture Patterns for Production&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;~9 min read · Queue design · Job architecture · Background processing&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;Background jobs are one of those features that feel solved the moment they work. You dispatch, it runs in the background, life is good. The framework handles it.&lt;/p&gt;

&lt;p&gt;The illusion holds until scale arrives. Then a password reset email sits behind a 90-second video compression job. A job that hits a flaky external API fails, retries, fails, retries — consuming worker processes while real work waits. A bulk operation dispatches 5,000 jobs at once and the queue system staggers under the spike. A crash halfway through a file operation leaves corrupted data you do not notice for three days.&lt;/p&gt;

&lt;p&gt;None of these are framework failures. They are design failures — decisions that were not made explicitly, so they defaulted to the wrong thing.&lt;/p&gt;

&lt;p&gt;This article is about the decisions, and the reasoning behind them.&lt;/p&gt;


&lt;h2&gt;
  
  
  What you will learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why passing an Eloquent model to a job constructor is wrong — and what to pass instead&lt;/li&gt;
&lt;li&gt;How to design a queue topology before you are forced to by a problem&lt;/li&gt;
&lt;li&gt;What retry strategy to use for transient, rate-limited, and permanent failures&lt;/li&gt;
&lt;li&gt;How to write file operations that leave no corrupted state when a job crashes mid-write&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  The mental model: a queue is not a to-do list
&lt;/h2&gt;

&lt;p&gt;Most queue problems come from treating a queue as a simple list — things go in, things come out, in order. That model works fine for small volumes. It breaks under pressure.&lt;/p&gt;

&lt;p&gt;The more accurate mental model: a queue is a &lt;strong&gt;work contract&lt;/strong&gt; between the part of your system that knows something needs to happen and the part that will do it — separated by time, by process restarts, and by failures. The job payload is a self-contained instruction. It cannot assume anything about the context that created it still exists.&lt;/p&gt;

&lt;p&gt;That one shift — from "list of tasks" to "self-contained work contract" — makes the rest of this article's decisions feel obvious rather than arbitrary.&lt;/p&gt;


&lt;h2&gt;
  
  
  What to put in a job constructor — and what not to
&lt;/h2&gt;

&lt;p&gt;The most frequent queue mistake is passing an Eloquent model to a job constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This seems fine. It is not.&lt;/span&gt;
&lt;span class="nc"&gt;CompressVideo&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;$media&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a job is dispatched, its constructor arguments are serialized into the queue payload. Laravel's &lt;code&gt;SerializesModels&lt;/code&gt; trait handles this by storing the model's class and primary key — and re-fetching the model when the job runs. That sounds correct, but consider: the job might run 30 seconds later. Or three hours later if the queue is backed up. The record may have been updated, soft-deleted, or had its status changed in the time between dispatch and execution.&lt;/p&gt;

&lt;p&gt;If the job carries a serialized model, it works with the snapshot from dispatch time. If the job carries an ID, it fetches current data at execution time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompressMediaVideo&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;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$mediaId&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;VideoCompressorService&lt;/span&gt; &lt;span class="nv"&gt;$compressor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Fetch fresh at execution time, not stale from dispatch time&lt;/span&gt;
        &lt;span class="nv"&gt;$media&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Media&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;mediaId&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;$media&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mime_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'video/'&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="c1"&gt;// State may have changed since dispatch — handle gracefully&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$compressor&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$media&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;Pass IDs, not models. The job's responsibility is to do a unit of work starting from first principles — not to continue a conversation that started somewhere else.&lt;/p&gt;




&lt;h2&gt;
  
  
  The single responsibility principle, applied to jobs
&lt;/h2&gt;

&lt;p&gt;A job class should do one thing. This sounds obvious. It is regularly violated.&lt;/p&gt;

&lt;p&gt;The problem with jobs that do multiple things: when one part fails, the whole job fails. If a job compresses a video, updates the database, sends a notification, and syncs to an external system, and the external sync times out — the compression was done, but the job retries and runs compression again. The retry was meant to retry the sync. You have wasted CPU, potentially introduced a duplicate notification, and made the job's retry behaviour unpredictable.&lt;/p&gt;

&lt;p&gt;The cleaner design — one job per responsibility, chained. Note that the constructor is still required on each class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompressMediaVideo&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;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$mediaId&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;VideoCompressorService&lt;/span&gt; &lt;span class="nv"&gt;$compressor&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;$media&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Media&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;mediaId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$compressor&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$media&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Compression done — dispatch the next unit of work independently&lt;/span&gt;
        &lt;span class="nc"&gt;UpdateMediaMetadata&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;mediaId&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;onQueue&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each job is independently retryable. A failure in metadata update does not re-run compression. You can add monitoring per job type and scale workers for specific job types independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Queue topology: the decision nobody makes until they have to
&lt;/h2&gt;

&lt;p&gt;A single &lt;code&gt;default&lt;/code&gt; queue works until it does not. The moment you have both time-sensitive operations (password resets, OTPs) and long-running operations (report generation, bulk compression) on the same queue, you have a priority problem the queue itself cannot solve.&lt;/p&gt;

&lt;p&gt;The topology decision — which queues exist and what runs on each — should be made before the first job is deployed, not when a user complains their OTP never arrived because it was stuck behind a batch export.&lt;/p&gt;

&lt;p&gt;A practical topology:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Queue&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Acceptable delay&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;critical&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Password resets, OTPs, security alerts&lt;/td&gt;
&lt;td&gt;None — delay means users cannot access the system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Notifications, status updates, lightweight tasks&lt;/td&gt;
&lt;td&gt;A few seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;media&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Video compression, image processing&lt;/td&gt;
&lt;td&gt;Minutes — slow and CPU-intensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;External API sync jobs&lt;/td&gt;
&lt;td&gt;Variable — isolate third-party failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bulk exports, large queries&lt;/td&gt;
&lt;td&gt;Can take minutes — must never starve other queues&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Workers listen to queues in priority order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Critical has its own dedicated worker — nothing else competes&lt;/span&gt;
php artisan queue:work redis &lt;span class="nt"&gt;--queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;critical &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30

&lt;span class="c"&gt;# This worker handles default first, then media if default is empty&lt;/span&gt;
php artisan queue:work redis &lt;span class="nt"&gt;--queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default,media &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120

&lt;span class="c"&gt;# Sync worker — isolated so third-party failures never affect your own queues&lt;/span&gt;
php artisan queue:work redis &lt;span class="nt"&gt;--queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;60

&lt;span class="c"&gt;# Reports isolated — slow timeouts do not affect other workers&lt;/span&gt;
php artisan queue:work redis &lt;span class="nt"&gt;--queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;reports &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why isolate &lt;code&gt;sync&lt;/code&gt;?&lt;/strong&gt; External API reliability is outside your control. If a third-party goes down and you are retrying aggressively, those retry jobs accumulate in the queue. On a shared queue, they compete for worker processes with your own jobs. An isolated &lt;code&gt;sync&lt;/code&gt; queue means a third-party outage only affects sync workers — your application's own background work continues unaffected.&lt;/p&gt;

&lt;p&gt;If you are on Redis and can run a dashboard, &lt;a href="https://laravel.com/docs/horizon" rel="noopener noreferrer"&gt;Laravel Horizon&lt;/a&gt; gives you queue depth, throughput, and failed job monitoring out of the box. If you are on a database driver or a locked environment, a &lt;code&gt;php artisan queue:failed&lt;/code&gt; count in a scheduled log check covers the essentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  Retry strategy: not all failures are equal
&lt;/h2&gt;

&lt;p&gt;The default retry behaviour — try N times, then give up — is right for some jobs and wrong for others. Thinking about failure modes before writing the job saves you from the wrong defaults.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transient failures
&lt;/h3&gt;

&lt;p&gt;Database hiccups, brief network timeouts — things that will probably succeed on retry:&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="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Wait 5 seconds between retries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rate-limited or service-down failures
&lt;/h3&gt;

&lt;p&gt;External API unavailable, rate limit hit — back off with increasing delays:&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="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;backoff&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="c1"&gt;// 30s, 60s, 120s, 240s, 480s between retries&lt;/span&gt;
    &lt;span class="k"&gt;return&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="mi"&gt;60&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="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;480&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;retryUntil&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;\DateTime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Regardless of tries, give up after 24 hours&lt;/span&gt;
    &lt;span class="c1"&gt;// Prevents zombie jobs accumulating during prolonged outages&lt;/span&gt;
    &lt;span class="k"&gt;return&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;addHours&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Permanent failures
&lt;/h3&gt;

&lt;p&gt;Malformed input, a record that will never exist — do not retry at all:&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="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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="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="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Alert immediately — this needs human attention, not retry&lt;/span&gt;
    &lt;span class="nf"&gt;logger&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;critical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Permanent job failure'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'job'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;static&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;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;&lt;strong&gt;Why &lt;code&gt;timeout&lt;/code&gt; and &lt;code&gt;tries&lt;/code&gt; are different things, and why it matters:&lt;/strong&gt; A job that hits &lt;code&gt;$timeout&lt;/code&gt; is killed and returns to the queue — it counts as a retry. A job that exhausts &lt;code&gt;$tries&lt;/code&gt; goes to &lt;code&gt;failed_jobs&lt;/code&gt;. If you set &lt;code&gt;timeout = 120&lt;/code&gt; and &lt;code&gt;tries = 3&lt;/code&gt; on a job that consistently takes 130 seconds, it will time out three times and land in &lt;code&gt;failed_jobs&lt;/code&gt; — never completing, consuming three worker-slots in the process.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake:&lt;/strong&gt; Setting &lt;code&gt;$timeout&lt;/code&gt; lower than the job's typical execution time. Every time the job runs, it gets killed before finishing, counts as a retry, and eventually exhausts &lt;code&gt;$tries&lt;/code&gt; without ever succeeding. If a job needs 90 seconds, give it at least 120. The timeout is a safety net, not a target.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Processing large backlogs: the Artisan command pattern
&lt;/h2&gt;

&lt;p&gt;When you need to dispatch jobs for thousands of existing records — a backlog compression, a data migration, a bulk recalculation — the dispatch mechanism is as important as the job itself.&lt;/p&gt;

&lt;p&gt;The pattern: an Artisan command that chunks through the database and dispatches jobs incrementally, with a &lt;code&gt;--dry-run&lt;/code&gt; option before you commit to anything:&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;ProcessBacklogCommand&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'media:compress-videos
        {--id=      : Process a single record by ID}
        {--dry-run  : Show what would run without dispatching anything}
        {--chunk=200: Records per batch}'&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="c1"&gt;// If --id is provided, scope the query to that single record&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Media&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mime_type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'like'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'video/%'&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;whereNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'compressed_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;when&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;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;option&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="nv"&gt;$total&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="nb"&gt;count&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;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Found &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; records to process."&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;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dry-run'&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;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Dry run complete — no jobs dispatched.'&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;$dispatched&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;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chunkById&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'chunk'&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;$batch&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;$dispatched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$batch&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;CompressMediaVideo&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;$record&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="nf"&gt;onQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'media'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nv"&gt;$dispatched&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Dispatched &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$dispatched&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; jobs so far..."&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;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Done. Total dispatched: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$dispatched&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;&lt;strong&gt;Why &lt;code&gt;chunkById&lt;/code&gt; instead of &lt;code&gt;chunk&lt;/code&gt;?&lt;/strong&gt; Laravel's &lt;code&gt;chunk()&lt;/code&gt; uses SQL &lt;code&gt;OFFSET&lt;/code&gt; — which means the database must count and skip N rows before returning the next batch. On a table with a million rows, the 500th batch requires skipping 100,000 rows. It gets progressively slower. &lt;code&gt;chunkById&lt;/code&gt; uses &lt;code&gt;WHERE id &amp;gt; $lastSeen LIMIT $chunk&lt;/code&gt; — a keyset cursor. The query time is constant regardless of position. For large tables, this is the difference between a command that completes in minutes and one that times out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;--dry-run&lt;/code&gt; as a first-class option?&lt;/strong&gt; Because the first time you run a bulk dispatch against production data, you want to see what would happen before it happens. &lt;code&gt;--dry-run&lt;/code&gt; costs nothing to add and saves numerous "I did not realise it would dispatch 40,000 jobs" moments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Atomic file operations: designing for crashes
&lt;/h2&gt;

&lt;p&gt;Any job that writes a file must assume it will crash mid-write. At scale, it happens. The question is what state the system is in when it does.&lt;/p&gt;

&lt;p&gt;Write directly to the target path: a crash leaves a partial, corrupted file where the original was. The damage is permanent until manual intervention.&lt;/p&gt;

&lt;p&gt;Write to a temporary file, then rename:&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;$inputPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$media&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;full_path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$tempPath&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$inputPath&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.tmp.'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;uniqid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// All writes go here — a crash here leaves the original untouched&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;runFFmpeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$inputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tempPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Only replace if the output is actually better&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;filesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tempPath&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;filesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$inputPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tempPath&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="c1"&gt;// Original was already optimal — do not replace it&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// rename() is atomic on POSIX filesystems — this either completes&lt;/span&gt;
&lt;span class="c1"&gt;// or does not happen. There is no in-between state.&lt;/span&gt;
&lt;span class="nb"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$inputPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why compare sizes before replacing?&lt;/strong&gt; Re-encoding an already-compressed file can produce a larger output — the encoding overhead outweighs the savings. Always verify the output is an improvement before discarding the original.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is &lt;code&gt;rename()&lt;/code&gt; atomic?&lt;/strong&gt; On Linux and macOS, &lt;code&gt;rename()&lt;/code&gt; is a single filesystem syscall. The kernel guarantees it either moves the file or does not — there is no half-way state where the destination file is partially written. The old file remains intact until the new one is fully in place.&lt;/p&gt;




&lt;h2&gt;
  
  
  The shape of a well-designed job
&lt;/h2&gt;

&lt;p&gt;Bringing it together:&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;CompressMediaVideo&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&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;$tries&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="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="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Takes an ID, not a model&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$mediaId&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;VideoCompressorService&lt;/span&gt; &lt;span class="nv"&gt;$compressor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Fetches fresh at execution time&lt;/span&gt;
        &lt;span class="nv"&gt;$media&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Media&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;mediaId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Guards for state changes since dispatch&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;$media&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mime_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'video/'&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;$compressor&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$media&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Failure handling is not an afterthought&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="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;logger&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Video compression failed after all retries'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'media_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mediaId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'error'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$exception&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This job takes an ID not a model, fetches fresh data at execution time, guards against state changes since dispatch, has a meaningful &lt;code&gt;failed()&lt;/code&gt; handler, and has timeout, retry, and backoff values that reflect actual job characteristics — not framework defaults.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The key insight from this article:&lt;/strong&gt; Queue failures are almost never framework failures — they are design failures. Passing IDs not models, setting explicit timeout and retry values, and separating work by queue type are decisions that should be made before the first job is deployed, not after the first incident.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before you ship — checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Queue topology is defined — not everything on &lt;code&gt;default&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Every job constructor takes an ID, not an Eloquent model&lt;/li&gt;
&lt;li&gt;[ ] Every job has &lt;code&gt;$tries&lt;/code&gt;, &lt;code&gt;$timeout&lt;/code&gt;, and &lt;code&gt;$backoff&lt;/code&gt; explicitly set — not left to framework defaults&lt;/li&gt;
&lt;li&gt;[ ] Every job has a &lt;code&gt;failed()&lt;/code&gt; method that logs meaningful context&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;$timeout&lt;/code&gt; is comfortably higher than the job's typical execution time&lt;/li&gt;
&lt;li&gt;[ ] Bulk dispatch commands use &lt;code&gt;chunkById&lt;/code&gt;, not &lt;code&gt;chunk&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Bulk commands have a &lt;code&gt;--dry-run&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;[ ] Any job that writes a file writes to &lt;code&gt;.tmp&lt;/code&gt; first, then &lt;code&gt;rename()&lt;/code&gt; — never directly to the target path&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Previous: &lt;a href="https://blog.shakiltech.com/laravel-audit-trail-building-a-system-that-remembers/" rel="noopener noreferrer"&gt;Part 1 — The Audit Trail&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
    </item>
    <item>
      <title>The Audit Trail: Building a System That Remembers</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Fri, 17 Apr 2026 16:33:26 +0000</pubDate>
      <link>https://dev.to/itxshakil/the-audit-trail-building-a-system-that-remembers-4bh0</link>
      <guid>https://dev.to/itxshakil/the-audit-trail-building-a-system-that-remembers-4bh0</guid>
      <description>&lt;p&gt;&lt;strong&gt;Part 1 of 4 — Laravel Architecture Patterns for Production&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;~10 min read · Compliance · Model logging · Request tracing&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;A transaction record had been modified.&lt;/p&gt;

&lt;p&gt;The amount was different from what the user had submitted. Support escalated it. The user denied changing anything. The developer who last touched the code was on leave. We looked at the database — the record had an &lt;code&gt;updated_at&lt;/code&gt; from two days ago and a different value than expected. That was everything we had.&lt;/p&gt;

&lt;p&gt;No who. No which fields. No context about what request caused it.&lt;/p&gt;

&lt;p&gt;We had a working application. We did not have a system that remembered anything.&lt;/p&gt;

&lt;p&gt;That was the incident that made us build this.&lt;/p&gt;


&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A request ID generated in middleware that automatically appears in every log line for that request — no manual threading required&lt;/li&gt;
&lt;li&gt;Field-level model diffs that capture what changed &lt;em&gt;from&lt;/em&gt; and &lt;em&gt;to&lt;/em&gt;, not just that a record was updated&lt;/li&gt;
&lt;li&gt;Append-only log files segmented to stay fast at millions of records&lt;/li&gt;
&lt;li&gt;A Gate hook that logs failed permission checks before they become incidents&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What an audit trail actually needs to prove
&lt;/h2&gt;

&lt;p&gt;Before writing code, it is worth being precise about what you are building — because "audit trail" means different things in different contexts, and the requirements determine the architecture.&lt;/p&gt;

&lt;p&gt;If you need a queryable, database-backed audit log with a ready-made API, &lt;a href="https://github.com/spatie/laravel-activitylog" rel="noopener noreferrer"&gt;spatie/laravel-activitylog&lt;/a&gt; is the standard choice and it is well built. What follows is for when your requirements go further — append-only guarantees, field-level diffs, and audit records that are genuinely hard to tamper with.&lt;/p&gt;

&lt;p&gt;In a compliance-heavy environment — fintech, healthcare, any regulated domain — an audit trail needs to answer four questions about any data change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt; made the change (user ID, IP address, user agent)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; changed — not "the record was updated," but which fields, from what value to what&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When&lt;/strong&gt; it changed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it is trustworthy&lt;/strong&gt; — the audit record itself cannot be quietly modified&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most implementations get the first three. The fourth is where they fail, and it is the one that matters most in an actual audit.&lt;/p&gt;

&lt;p&gt;Here is the problem with a database audit table: your application code writes to it. That means your application code can also &lt;code&gt;UPDATE&lt;/code&gt; it. A table that application code can modify is not an immutable record — it is a mutable history. An auditor who understands this will ask how you prevent tampering, and "we trust our own code" is not a satisfying answer.&lt;/p&gt;

&lt;p&gt;This shapes the decision to use file-based logging. But first, there is a more foundational problem: correlation.&lt;/p&gt;


&lt;h2&gt;
  
  
  The request ID: one thread through every log
&lt;/h2&gt;

&lt;p&gt;A model change does not happen in isolation. It happens during a request — a specific HTTP call from a specific user at a specific moment. Without connecting the model change to that request, you have timestamped facts with no story between them.&lt;/p&gt;

&lt;p&gt;The solution is a request ID: a UUID generated at the start of every request and written into every log line that request produces.&lt;/p&gt;

&lt;p&gt;Before building the middleware class, add the &lt;code&gt;request&lt;/code&gt; and &lt;code&gt;query&lt;/code&gt; channels to &lt;code&gt;config/logging.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/logging.php&lt;/span&gt;
&lt;span class="s1"&gt;'channels'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;

    &lt;span class="c1"&gt;// ... your existing channels ...&lt;/span&gt;

    &lt;span class="s1"&gt;'request'&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;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'daily'&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="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logs/request.log'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'level'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'debug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'days'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Retain 90 days — adjust to your compliance requirement&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'query'&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;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'daily'&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="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logs/query.log'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'level'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'debug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'days'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the middleware:&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;RequestLogger&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;const&lt;/span&gt; &lt;span class="no"&gt;HEADER_NAME&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'X-Request-Id'&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;const&lt;/span&gt; &lt;span class="no"&gt;REQUEST_ID_ATTRIBUTE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'request_id'&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="nv"&gt;$requestId&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// Store in request attributes for internal access within the same request&lt;/span&gt;
        &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;attributes&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="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;REQUEST_ID_ATTRIBUTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$requestId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Also set as a header — downstream systems and the browser can read it&lt;/span&gt;
        &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;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="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HEADER_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$requestId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// shareContext injects request_id into every Log:: call&lt;/span&gt;
        &lt;span class="c1"&gt;// for the rest of this request automatically — no manual threading required&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;shareContext&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'request_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$requestId&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="c1"&gt;// Monotonic clock — more accurate than microtime()&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;$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="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HEADER_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$requestId&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;logRequest&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;$response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$startedAt&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;logRequest&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;Response&lt;/span&gt; &lt;span class="nv"&gt;$response&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;$requestId&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;$startedAt&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;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'request'&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;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'request.completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'request_id'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'method'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;method&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;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPathInfo&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="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStatusCode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'duration_ms'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;round&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'user_id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$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="nf"&gt;getAuthIdentifier&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'ip'&lt;/span&gt;          &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ip&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;Register it as global middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Laravel 11 — bootstrap/app.php&lt;/span&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="kt"&gt;Middleware&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;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\App\Http\Middleware\RequestLogger&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="c1"&gt;// Laravel 10 and earlier — app/Http/Kernel.php&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$middleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nc"&gt;\App\Http\Middleware\RequestLogger&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;Every log entry your application writes from this point on will now include the request ID automatically. Here is what a request log entry looks like in &lt;code&gt;storage/logs/request.log&lt;/code&gt;:&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"request.completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9d4e2f1a-83bc-4a7c-b291-7e5f3d9a1c84"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PATCH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/transactions/1101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;43.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INFO"&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;And a model change log entry in &lt;code&gt;storage/logs/activities/transactions/1100/transaction_1101.log&lt;/code&gt;:&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;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9d4e2f1a-83bc-4a7c-b291-7e5f3d9a1c84"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-03-15 14:32:07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changes"&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;"amount"&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;"old"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5000.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"new"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"500.00"&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;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 ..."&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;The same &lt;code&gt;request_id&lt;/code&gt; appears in both files. That is the correlation. When a support ticket says "something changed on Transaction 1101," you grep that ID across both logs and have the complete picture immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;Log::shareContext()&lt;/code&gt; instead of passing the ID manually everywhere?&lt;/strong&gt; &lt;a href="https://laravel.com/docs/logging#contextual-information" rel="noopener noreferrer"&gt;&lt;code&gt;shareContext()&lt;/code&gt;&lt;/a&gt; injects the given data into every &lt;code&gt;Log::&lt;/code&gt; call for the rest of the request lifecycle automatically. You do not thread the request ID through your services, model observers, or Gate hooks. Whatever you log, the request ID is already there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why UUID instead of a random integer?&lt;/strong&gt; A six-digit random int has a real collision probability under concurrent load. A UUID is 128 bits of randomness — collision is not a realistic concern. The ID also goes into the response header (&lt;code&gt;X-Request-Id&lt;/code&gt;), so a user who raises a support ticket can include it and you find the exact request in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a single structured entry after the response rather than separate request-in and response-out entries?&lt;/strong&gt; One entry per request gives you the complete picture — method, path, status code, duration — in one line without cross-referencing. The &lt;code&gt;hrtime(true)&lt;/code&gt; monotonic clock is more accurate for duration measurement than &lt;code&gt;microtime()&lt;/code&gt; because it is not affected by system clock adjustments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on &lt;code&gt;X-Forwarded-For&lt;/code&gt;:&lt;/strong&gt; When your application sits behind a load balancer, &lt;code&gt;request()-&amp;gt;ip()&lt;/code&gt; returns the proxy's IP, not the user's. The &lt;code&gt;TrustProxies&lt;/code&gt; middleware resolves this — once configured, &lt;code&gt;request()-&amp;gt;ip()&lt;/code&gt; returns the real client IP. Pass it to external API calls so downstream system logs also record the real user, not your server's address.&lt;/p&gt;




&lt;h2&gt;
  
  
  Slow queries: while you are here, add this
&lt;/h2&gt;

&lt;p&gt;This is a separate concern from the audit trail — but since you are already setting up logging infrastructure, it costs almost nothing to add and consistently pays off the first time a performance problem hits production.&lt;/p&gt;

&lt;p&gt;The idea: log slow queries with severity levels matched to how serious the slowness actually is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AppServiceProvider.php&lt;/span&gt;
&lt;span class="c1"&gt;// Scope this to non-production environments or behind a config flag&lt;/span&gt;
&lt;span class="c1"&gt;// in high-traffic systems — DB::listen fires on every query.&lt;/span&gt;
&lt;span class="k"&gt;if&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="s1"&gt;'app.debug'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logging.query_listener'&lt;/span&gt;&lt;span class="p"&gt;))&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="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$time&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="k"&gt;match&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="p"&gt;{&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="mi"&gt;10000&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;channel&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Extremely slow (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$time&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms)"&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;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;  &lt;span class="o"&gt;=&amp;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;channel&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Very slow (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$time&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms)"&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;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;channel&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Slow (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$time&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms)"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Fast queries add noise without value&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why map duration to log level?&lt;/strong&gt; Because log levels mean something — or they should. If everything is &lt;code&gt;info&lt;/code&gt;, nothing stands out. A 12-second query logged as &lt;code&gt;critical&lt;/code&gt; will trigger any alert rule that fires on critical log entries without writing any additional monitoring code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the config gate?&lt;/strong&gt; &lt;code&gt;DB::listen&lt;/code&gt; fires on every single query. In a high-traffic production environment, the overhead is real. Gate it behind &lt;code&gt;app.debug&lt;/code&gt; or a dedicated config flag so you control when it runs.&lt;/p&gt;

&lt;p&gt;The 100ms warning threshold is the fastest way to surface missing indexes and N+1 problems. You see the warning in the logs, you add the index, the warning stops. No user ever had to complain.&lt;/p&gt;

&lt;p&gt;That said — this is optional. If you add nothing else from this section, the audit trail still works. The query listener is a low-cost addition that earns its place, not a requirement.&lt;/p&gt;




&lt;h2&gt;
  
  
  The model change logger
&lt;/h2&gt;

&lt;p&gt;With a request ID threading through the system, model-level changes become meaningful entries in a traceable story rather than isolated database facts.&lt;/p&gt;

&lt;p&gt;The implementation is a trait on any Eloquent model that needs auditing:&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;trait&lt;/span&gt; &lt;span class="nc"&gt;ModelChangeLogger&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;bootModelChangeLogger&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;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;updating&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;$model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_diff_assoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAttributes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOriginal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// updated_at is present in every update. It is never interesting.&lt;/span&gt;
            &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$changed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'updated_at'&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="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$changed&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="c1"&gt;// Build a field-level diff — not just the new values,&lt;/span&gt;
            &lt;span class="c1"&gt;// but what each field changed *from*&lt;/span&gt;
            &lt;span class="nv"&gt;$diff&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;$changed&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$newValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$diff&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$key&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;'old'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOriginal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'N/A'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'new'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$newValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;logChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$diff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'updated'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;created&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;$model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;logChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAttributes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'created'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;deleting&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;$model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Capture the full state before deletion — after deletion,&lt;/span&gt;
            &lt;span class="c1"&gt;// getAttributes() returns nothing useful&lt;/span&gt;
            &lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;logChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAttributes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'deleted'&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;prepareLogData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$changes&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;$event&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="c1"&gt;// Mask sensitive fields before writing — log that they changed,&lt;/span&gt;
        &lt;span class="c1"&gt;// not what they changed to&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;maskedAttributes&lt;/span&gt; &lt;span class="o"&gt;??&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;$attr&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="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$attr&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$attr&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;'old'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'[REDACTED]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'new'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'[REDACTED]'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'request_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;request&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;attributes&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="nc"&gt;RequestLogger&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;REQUEST_ID_ATTRIBUTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'N/A'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'time'&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;toDateTimeString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'model_id'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'changes'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'user_id'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'ip'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;request&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;ip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'user_agent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;request&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;userAgent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Override in each model to list fields that should never appear in plain text&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$maskedAttributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on console context:&lt;/strong&gt; When &lt;code&gt;ModelChangeLogger&lt;/code&gt; runs inside a queued job or a scheduled command rather than an HTTP request, &lt;code&gt;request()-&amp;gt;attributes-&amp;gt;get(REQUEST_ID_ATTRIBUTE)&lt;/code&gt; returns &lt;code&gt;'N/A'&lt;/code&gt; — that is the intentional fallback, not a bug. If you want correlation in background jobs, generate a job-level UUID in the job constructor and inject it via &lt;code&gt;Log::shareContext()&lt;/code&gt; at the start of &lt;code&gt;handle()&lt;/code&gt;, the same way &lt;code&gt;RequestLogger&lt;/code&gt; does for HTTP requests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;getOriginal()&lt;/code&gt; rather than just the new values?&lt;/strong&gt; Because "the &lt;code&gt;amount&lt;/code&gt; field is now 500" is half the story. "The &lt;code&gt;amount&lt;/code&gt; field changed from 5000 to 500" is evidence. In a dispute, the diff proves the change happened — not just the current state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why capture &lt;code&gt;deleting&lt;/code&gt; before the delete, not after?&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake:&lt;/strong&gt; Using &lt;code&gt;static::deleted&lt;/code&gt; instead of &lt;code&gt;static::deleting&lt;/code&gt; for the deletion hook. &lt;code&gt;deleted&lt;/code&gt; fires after the row is gone — &lt;code&gt;getAttributes()&lt;/code&gt; returns nothing. Always use &lt;code&gt;deleting&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;deleting&lt;/code&gt; fires before the row is removed — the full record is still in memory. &lt;code&gt;deleted&lt;/code&gt; fires after, and &lt;code&gt;getAttributes()&lt;/code&gt; returns an empty array at that point.&lt;/p&gt;




&lt;h2&gt;
  
  
  File-based logs and the folder segmentation problem
&lt;/h2&gt;

&lt;p&gt;Here is the decision that shapes the rest of the logging architecture: where do the log entries live?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The database option&lt;/strong&gt; is tempting — it is queryable, indexable, and fits naturally into a Laravel application. The problem: any table your application can write to, your application can also modify. An &lt;code&gt;UPDATE audit_logs&lt;/code&gt; is valid SQL. In a regulated environment, mutable audit records are a compliance liability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Append-only log files&lt;/strong&gt; are harder to tamper with. &lt;code&gt;FILE_APPEND&lt;/code&gt; at the OS level means every write goes to the end — there is no update operation, only add. Combined with filesystem permissions where the web user can write but not delete, you have logs that are genuinely difficult to alter.&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;logChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$changes&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;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$logPath&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;buildLogPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;$logPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;json_encode&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;prepareLogData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;FILE_APPEND&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;LOCK_EX&lt;/span&gt;  &lt;span class="c1"&gt;// LOCK_EX prevents concurrent writes corrupting the file&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake:&lt;/strong&gt; Using &lt;code&gt;FILE_APPEND&lt;/code&gt; alone. On a busy system where multiple requests modify records simultaneously, two processes can interleave their writes and corrupt a log entry. &lt;code&gt;LOCK_EX&lt;/code&gt; acquires an exclusive lock — one write completes fully before the next begins. Both flags are required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The folder structure problem:&lt;/strong&gt; if you store each record's log in a folder named after the table, you eventually have &lt;code&gt;transactions/&lt;/code&gt; containing one file per transaction. A million transactions means a million files in one directory. Most filesystems handle this technically, but &lt;code&gt;ls&lt;/code&gt;, backup tools, and directory enumeration operations become painfully slow.&lt;/p&gt;

&lt;p&gt;The solution is segmentation — group records into subdirectories by ID range:&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;buildLogPath&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="nv"&gt;$id&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;getKey&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;=&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;getTable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Segment folder: floor to the nearest 100&lt;/span&gt;
    &lt;span class="c1"&gt;// IDs 1–99 → folder "0", IDs 100–199 → folder "100", IDs 1100–1199 → folder "1100"&lt;/span&gt;
    &lt;span class="nv"&gt;$segment&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="nb"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$folder&lt;/span&gt; &lt;span class="o"&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="s2"&gt;"logs/activities/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$table&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;$segment&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$folder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$folder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mo"&gt;0755&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="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$filename&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="nf"&gt;class_basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="mf"&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;$id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.log"&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;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$folder&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;$filename&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;storage/logs/activities/
    transactions/
        0/
            transaction_1.log
            transaction_42.log
        1100/
            transaction_1101.log
            transaction_1102.log
        1200/
            transaction_1200.log
    users/
        0/
            user_1.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each folder contains at most 100 files. Finding a specific record's history means knowing its ID — which you always do — and reading one file. The structure scales to any number of records without any folder becoming unwieldy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Masking sensitive fields
&lt;/h2&gt;

&lt;p&gt;An audit trail that contains plaintext national ID numbers or passwords creates its own compliance problem. In many jurisdictions, a log file with unredacted PII is treated like any other PII store — subject to retention limits, access controls, and deletion rights.&lt;/p&gt;

&lt;p&gt;The approach here records that the field changed without recording what it changed to. An auditor can see that &lt;code&gt;national_id&lt;/code&gt; was modified on Transaction 1101 at 14:32 by User 42 — which satisfies the audit requirement — without the log file holding the actual values.&lt;/p&gt;

&lt;p&gt;Override &lt;code&gt;$maskedAttributes&lt;/code&gt; on any model that stores sensitive data:&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;Transaction&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&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;ModelChangeLogger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$maskedAttributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'national_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'card_number'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where RBAC fits into this picture
&lt;/h2&gt;

&lt;p&gt;Access control and audit trails are related problems. If you are logging who changed records, you should also log who &lt;em&gt;tried&lt;/em&gt; to do something they were not allowed to.&lt;/p&gt;

&lt;p&gt;Laravel's Gate provides a hook for exactly 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="nc"&gt;Gate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;after&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;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$ability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$result&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;$result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// request_id is automatically included via Log::shareContext()&lt;/span&gt;
        &lt;span class="c1"&gt;// set in RequestLogger — no need to fetch it manually here&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;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'request'&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;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Authorization denied'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'ability'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'ip'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;request&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;ip&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;&lt;strong&gt;Why log failures specifically?&lt;/strong&gt; A successful authorization attempt is normal operation. A failed attempt is a signal — a user may be probing endpoints they should not have access to, or a compromised account may be attempting privilege escalation. The pattern across multiple failed attempts becomes early warning you would not have without this hook.&lt;/p&gt;

&lt;p&gt;One design principle worth stating: design permissions around abilities, not role names. &lt;code&gt;$user-&amp;gt;can('transaction.approve')&lt;/code&gt; expresses what the user can do, regardless of what role label they carry. &lt;code&gt;$user-&amp;gt;role === 'admin'&lt;/code&gt; breaks the moment "admin" means different things in different contexts — and in growing systems, it always eventually does.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you now have
&lt;/h2&gt;

&lt;p&gt;Register &lt;code&gt;RequestLogger&lt;/code&gt; as global middleware. Add &lt;code&gt;ModelChangeLogger&lt;/code&gt; to any Eloquent model. Register the &lt;code&gt;Gate::after&lt;/code&gt; hook.&lt;/p&gt;

&lt;p&gt;Now: every request carries an ID. Every model change records who made it, which field changed from what value to what, when, and which request caused it. Failed permission checks are logged. Every log line across your stack shares a common correlation ID.&lt;/p&gt;

&lt;p&gt;When a support ticket arrives saying "something changed on Transaction 1101," you open &lt;code&gt;storage/logs/activities/transactions/1100/transaction_1101.log&lt;/code&gt;. You see the exact diff, the user who made it, the request ID. You grep that request ID in your request logs. You have the complete picture in under a minute.&lt;/p&gt;

&lt;p&gt;That is the difference between an application that works and one that remembers.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The key insight from this article:&lt;/strong&gt; A database audit table is mutable — your application code can UPDATE it. Append-only log files with a request ID threaded through every entry give you tamper-resistant, correlated records that answer who, what, and why in one grep.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before you ship — checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;RequestLogger&lt;/code&gt; registered as global middleware in &lt;code&gt;bootstrap/app.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;ModelChangeLogger&lt;/code&gt; trait added to every model that handles sensitive or auditable data&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;$maskedAttributes&lt;/code&gt; defined on models that store PII (passwords, national IDs, card numbers)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;storage/logs/activities/&lt;/code&gt; is writable by the web user, not deletable&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;DB::listen&lt;/code&gt; is gated behind a config flag or &lt;code&gt;app.debug&lt;/code&gt; — not running unconditionally in production&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;Gate::after&lt;/code&gt; hook registered in &lt;code&gt;AuthServiceProvider&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] A grep for a known request ID returns results across both the request log and model change logs&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Next: &lt;a href="//./part-2-queue-architecture.md"&gt;Part 2 — Queue Architecture: Designing Background Work That Holds Up&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>Laravel Architecture Patterns for Production</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Fri, 17 Apr 2026 16:28:52 +0000</pubDate>
      <link>https://dev.to/itxshakil/laravel-architecture-patterns-for-production-19f3</link>
      <guid>https://dev.to/itxshakil/laravel-architecture-patterns-for-production-19f3</guid>
      <description>&lt;p&gt;Most Laravel applications work. Routes respond, data gets saved, users can log in. The framework handles it.&lt;/p&gt;

&lt;p&gt;Then the hard questions arrive. &lt;em&gt;Can you prove that transaction wasn't tampered with?&lt;/em&gt; Or: &lt;em&gt;what happens when a bulk operation dispatches 5,000 jobs overnight?&lt;/em&gt; Or: &lt;em&gt;what happens to our users when that third-party API goes down?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These are the questions that separate a Laravel application that works from one that holds up.&lt;/p&gt;

&lt;p&gt;This series is built from real production code — written after those questions were asked, after something went wrong, after a requirement changed in a way the original architecture couldn't handle. Every pattern came from a real system under real pressure: fintech, regulated environments, places where "it works" is not a sufficient answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; You have shipped Laravel to production. You know the framework. Now you are being asked to make the system serious — audit-ready, reliable under load, and defensible under scrutiny. You want the reasoning behind the patterns so you can adapt them, not just the code to copy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; PHP 8.0+ and Laravel 9+ for all four parts. Part 4 uses &lt;code&gt;Illuminate\Support\Facades\Context&lt;/code&gt;, which requires Laravel 11 — a compatible fallback for earlier versions is shown in that article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On scope:&lt;/strong&gt; These patterns came from specific production environments and reflect specific tradeoffs. They are documented decisions, not universal prescriptions. Each article names the tradeoffs and labels the common mistakes so you can make an informed choice for your own context.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Four Parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://blog.shakiltech.com/laravel-audit-trail-building-a-system-that-remembers/" rel="noopener noreferrer"&gt;Part 1 — The Audit Trail: Building a System That Remembers&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;~10 min read · Compliance · Model logging · Request tracing&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A request ID generated in middleware that automatically appears in every log line for that request. Field-level model diffs that capture what changed from what value to what — not just that a record was updated. Append-only log files segmented to stay fast at scale. A Gate hook that logs failed permission checks before they become incidents.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A database audit table is mutable. Append-only files are not. That one sentence shapes the entire architecture.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part 2 — Queue Architecture: Designing Background Work That Holds Up
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;~9 min read · Queue design · Job architecture · Background processing&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Why passing an Eloquent model to a job constructor is the wrong call, and what to pass instead. How to design a queue topology before you are forced to by a problem. Retry strategies matched to transient, rate-limited, and permanent failures. File operations written to survive a crash mid-write.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A password reset email stuck behind a video compression job is not a queue problem. It is a topology problem.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part 3 — Secure File Uploads: Seven Checks and Why Each One Exists
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;~9 min read · Security · Middleware · File handling&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A middleware that validates seven independent properties of every uploaded file, each one named and explained. Server-side MIME detection using &lt;code&gt;finfo&lt;/code&gt; — not the browser's claim. A unique name strategy that removes user-controlled strings from filesystem paths entirely. A storage pattern where no uploaded file is ever directly reachable via HTTP.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;getClientMimeType()&lt;/code&gt; returns whatever the browser sent. &lt;code&gt;finfo&lt;/code&gt; reads the actual bytes. Only one of those is a security check.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part 4 — External API Reliability: When Their System Goes Down
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;~9 min read · Integration architecture · Resilience · Fault tolerance&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A trait that makes the fallback decision — fail loud or fail silent — explicit in the model, not buried in a catch block. A recovery path for operations that completed externally but failed locally. The idempotency question, and why it matters when the database write fails after the external API already accepted the request.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The 2am outage will still happen. The question is whether your system has a designed response or an improvised one.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Each article stands on its own. But if you are reading from the start, Part 1 establishes a request ID that runs as a thread through every subsequent part.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>How I Reduced Video Storage by 30GB/Day Using FFmpeg and Laravel</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Tue, 07 Apr 2026 16:09:11 +0000</pubDate>
      <link>https://dev.to/itxshakil/how-i-reduced-video-storage-by-30gbday-using-ffmpeg-and-laravel-4im2</link>
      <guid>https://dev.to/itxshakil/how-i-reduced-video-storage-by-30gbday-using-ffmpeg-and-laravel-4im2</guid>
      <description>&lt;p&gt;Most teams don't realize how expensive "just 30 seconds of video" can become — until it silently turns into terabytes of storage and a massive monthly bill.&lt;/p&gt;

&lt;p&gt;That's exactly what happened to us.&lt;/p&gt;

&lt;p&gt;We were storing millions of short verification clips, each only ~30 seconds long — but together, they were costing us hundreds of GBs every month.&lt;/p&gt;

&lt;p&gt;Instead of throwing money at storage, we decided to fix the root problem.&lt;/p&gt;

&lt;p&gt;The result?&lt;br&gt;
👉 20–30 GB saved per day&lt;br&gt;
👉 60% average compression&lt;br&gt;
👉 Zero impact on review quality&lt;/p&gt;



&lt;p&gt;At my company, we run a video-based verification flow — users record short clips (typically 30 seconds) that get reviewed by our team. Simple enough. But over time, those clips add up fast.&lt;/p&gt;

&lt;p&gt;We had millions of recorded videos in storage with a retention requirement of 2–4 years. The storage bill kept climbing. The turning point came when I actually measured what we were storing — and realized we were saving far more data than we needed to.&lt;/p&gt;

&lt;p&gt;This is the story of how I built a pipeline that now compresses 2,000+ videos per day and saves 20–30 GB of storage daily — without touching a single live file.&lt;/p&gt;


&lt;h2&gt;
  
  
  Real Numbers from Production
&lt;/h2&gt;

&lt;p&gt;Before going into the how, here's what one day's run looks like on our compression dashboard:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Videos processed&lt;/td&gt;
&lt;td&gt;2,203&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total space saved&lt;/td&gt;
&lt;td&gt;15.66 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average compression ratio&lt;/td&gt;
&lt;td&gt;62.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best compression recorded&lt;/td&gt;
&lt;td&gt;99.92%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An average reduction of 62% in file size. Some clips (usually silent or nearly-static ones) compress all the way to 99.9%. Even on a typical day, we're saving 15+ GB from a single cron run.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem: Over-Engineered for the Wrong Use Case
&lt;/h2&gt;

&lt;p&gt;Our original recording setup used browser defaults — high resolution, high framerate, stereo audio at 48kHz. Reasonable general-purpose settings, but completely overkill for a 30-second face verification clip.&lt;/p&gt;

&lt;p&gt;When the backlog started growing into the millions of files, the cost became impossible to ignore. But before writing a single line of compression code, I needed to understand why the files were so large — and fix the source before compressing the backlog.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1: Research, Experiment, Validate
&lt;/h2&gt;

&lt;p&gt;I didn't just pick lower settings and ship them. I researched codec behavior, tested different bitrate and quality combinations, and — critically — validated with real recordings before touching user traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The validation approach:&lt;/strong&gt; Our verifiers (the people who review verification videos) also record their own video profiles. This was the perfect test population — small, controlled, and internal. I rolled out the new recording constraints to verifier accounts first and asked them to record 10-second clips, much longer than a typical verification video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 10 seconds?&lt;/strong&gt; Because problems with codec settings, device compatibility, or audio quality tend to hide in short clips but surface in longer ones. Buffer issues, audio sync drift, encoding artifacts under motion — these all become visible when you push the duration.&lt;/p&gt;

&lt;p&gt;Once the verifier videos came back clean across a range of devices (Chrome on Android, Chrome on desktop, Safari on iOS), I had confidence the settings were correct. Only then did I roll them out to user-facing recording.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2: Optimize Video Recording at the Source
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Understanding Browser Codec Support (VP8 vs VP9)
&lt;/h3&gt;

&lt;p&gt;Modern browsers support two primary video codecs for MediaRecorder:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VP8&lt;/strong&gt; — the older WebM codec. Widely supported across all browsers and platforms, including older Android devices. Less efficient compression than VP9 but extremely reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VP9&lt;/strong&gt; — Google's newer codec. Significantly better compression at the same quality level, especially for low-motion content like talking heads. Supported in Chrome, Firefox, and most modern browsers, but not always available on older devices or in certain Safari contexts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why fallback matters:&lt;/strong&gt; If you specify &lt;code&gt;video/webm;codecs=vp9,opus&lt;/code&gt; and the browser or device doesn't support VP9, MediaRecorder will either throw an error or silently produce a broken file. A hardcoded codec choice that works on your test device will fail on some percentage of real users' devices. You need to probe support at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mimeTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video/webm;codecs=vp9,opus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Best: VP9 video + Opus audio&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video/webm;codecs=vp8,opus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Good: VP8 video + Opus audio&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video/webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;                     &lt;span class="c1"&gt;// Fallback: browser chooses&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;mimeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mimeTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isTypeSupported&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MediaRecorder.isTypeSupported()&lt;/code&gt; checks at runtime whether the current browser/device combination actually supports a given MIME type. The list is ordered by preference — VP9 first, VP8 second, browser default last. Whatever the device supports, it gets the best available option.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual recording constraints
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;constraints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ideal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;640&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ideal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;480&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;frameRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ideal&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;channelCount&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="c1"&gt;// mono&lt;/span&gt;
    &lt;span class="na"&gt;echoCancellation&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="na"&gt;noiseSuppression&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="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mediaRecorder&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;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;videoBitsPerSecond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// 1 Mbps cap&lt;/span&gt;
  &lt;span class="na"&gt;audioBitsPerSecond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;         &lt;span class="c1"&gt;// 64 Kbps cap&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why each decision:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;640×480 at 24fps&lt;/strong&gt; — Sufficient to clearly identify a face. 30fps and 1080p are for content you'll watch repeatedly; a verification clip gets reviewed once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16kHz mono audio&lt;/strong&gt; — Voice intelligibility tops out around 8kHz. 16kHz captures speech clearly with half the data of 44.1kHz stereo. The verifier needs to hear what someone says, not enjoy high-fidelity audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 Mbps video / 64 Kbps audio&lt;/strong&gt; — A cap at the recorder level. Without this, VP8 in particular can spike well above reasonable bitrates on some devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;echoCancellation + noiseSuppression&lt;/strong&gt; — These reduce audio variance, which compresses better downstream. Noisy or echoey audio encodes less efficiently.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3: Compress the Backlog
&lt;/h2&gt;

&lt;p&gt;Fixing the source stopped the problem from growing. But millions of old files still existed at the original large sizes. I built a three-layer Laravel pipeline to work through them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1 — Artisan Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan media:compress-videos &lt;span class="nt"&gt;--chunk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200 &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# or target a single video for testing:&lt;/span&gt;
php artisan media:compress-videos &lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;202423
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command queries all records with a &lt;code&gt;video/%&lt;/code&gt; MIME type, chunks them into batches of 200, and dispatches a queued job per video. The &lt;code&gt;--dry-run&lt;/code&gt; flag lets you see what would be processed without doing anything — useful before running on production for the first time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2 — Queued Job
&lt;/h3&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;CompressMediaVideo&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;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="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;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;VideoCompressorService&lt;/span&gt; &lt;span class="nv"&gt;$compressor&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;$media&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Media&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;mediaId&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;$media&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mime_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'video/'&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="c1"&gt;// skip safely&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$compressor&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$media&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;One job per video. If a job fails, it retries independently without affecting the others. The compression run can take months on a backlog of millions — having individual, retryable jobs means a server restart or a bad file doesn't set you back to zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3 — Deep Dive into FFmpeg Compression Strategy
&lt;/h3&gt;

&lt;p&gt;This is where the actual compression happens. Some context on how it works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What FFmpeg is doing:&lt;/strong&gt; It reads the original WebM file (usually H.264 video + Opus audio, recorded by Chrome), re-encodes the video track using VP9, re-encodes the audio using Opus at a lower bitrate, and writes a new WebM container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why VP9 for compression?&lt;/strong&gt; VP9 uses much more sophisticated algorithms than H.264 — it's better at identifying redundant information across frames and removing it. For a mostly-static scene like a person talking against a wall, VP9 can throw away enormous amounts of data while keeping the image perfectly clear to a human reviewer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What CRF means:&lt;/strong&gt; CRF (Constant Rate Factor) is quality-controlled encoding. Instead of targeting a specific bitrate, you specify a quality level and FFmpeg decides how many bits are needed to achieve it. Lower CRF = higher quality = larger file. Higher CRF = more compression = smaller file.&lt;/p&gt;

&lt;p&gt;For VP9, CRF 33 is roughly "good quality." CRF 35 is "slightly aggressive." CRF 40+ starts to look visibly degraded. I chose CRF 35 because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a face verification clip, not a film&lt;/li&gt;
&lt;li&gt;The reviewer needs to see the face clearly, not count pores&lt;/li&gt;
&lt;li&gt;Our production data confirms it works — the 62.1% average compression ratio came from CRF 35
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'ffmpeg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$inputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'-c:v'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'libvpx-vp9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'-crf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'35'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'-b:v'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Required for CRF mode in VP9&lt;/span&gt;
    &lt;span class="s1"&gt;'-c:a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'libopus'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'-b:a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'64k'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'-y'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tempPath&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;-b:v 0&lt;/code&gt; is required when using CRF mode with &lt;code&gt;libvpx-vp9&lt;/code&gt;. Without it, VP9 defaults to a target bitrate mode and ignores the CRF value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Safe Processing with Atomic File Replacement
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$process&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exitCode&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="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tempPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Log and store error output for debugging&lt;/span&gt;
    &lt;span class="nv"&gt;$media&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'compressed_meta'&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;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$process&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getErrorOutput&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="c1"&gt;// Only replace the original after successful compression&lt;/span&gt;
&lt;span class="nb"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$inputPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FFmpeg writes to a &lt;code&gt;.tmp&lt;/code&gt; file first. If encoding fails for any reason — corrupt input, disk full, process killed — the original file is untouched. The &lt;code&gt;rename()&lt;/code&gt; only happens after a confirmed successful exit code. In a pipeline processing millions of files, this matters.&lt;/p&gt;

&lt;p&gt;The service also skips files that would grow: if the compressed output is larger than the original (which happens with already-optimized or very short files), &lt;code&gt;saved_bytes&lt;/code&gt; is recorded as 0 and the original is kept. The dashboard's &lt;code&gt;-130.33%&lt;/code&gt; low value represents exactly this case — a tiny file that got larger under re-encoding.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Storage Lifecycle Management (Trash + Cleanup Strategy)
&lt;/h2&gt;

&lt;p&gt;When a verification video is rejected and the user records a new one, we don't immediately delete the old clip. It gets moved to a &lt;code&gt;trash/&lt;/code&gt; directory. A weekly cron job permanently removes everything older than 7 days in trash.&lt;/p&gt;

&lt;p&gt;This gives the ops team a recovery window without keeping rejected videos indefinitely. It also keeps the compression pipeline clean — trash videos are excluded from the compression queue since they're headed for deletion anyway.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One day's run (March 11, 2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2,203 videos compressed&lt;/li&gt;
&lt;li&gt;15.66 GB saved&lt;/li&gt;
&lt;li&gt;62.1% average compression ratio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Projected at scale:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At ~20 GB/day average: 600 GB/month, 7+ TB/year&lt;/li&gt;
&lt;li&gt;The backlog has millions of videos — this pipeline will run for months&lt;/li&gt;
&lt;li&gt;New uploads are already smaller thanks to the recording constraint changes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate on internal users first.&lt;/strong&gt; Rolling out recording changes to verifier accounts before users meant any issues would be caught internally. Testing with 30-second clips specifically surfaces problems that short clips hide.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Probe codec support at runtime.&lt;/strong&gt; Never hardcode a MIME type. &lt;code&gt;MediaRecorder.isTypeSupported()&lt;/code&gt; is a one-liner that ensures every device gets the best encoding it can handle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CRF over bitrate caps.&lt;/strong&gt; A fixed bitrate wastes bits on simple scenes and starves complex ones. CRF adapts to content — simple talking-head frames get very small, complex frames get what they need.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-b:v 0&lt;/code&gt; is not optional for VP9 CRF.&lt;/strong&gt; This trips up a lot of people. VP9 in FFmpeg defaults to bitrate mode; CRF only activates when you explicitly set &lt;code&gt;-b:v 0&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Atomic writes protect your data.&lt;/strong&gt; In bulk processing, things fail. Write to temp, verify success, then rename. Never write directly to the live path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Queue per file, not per batch.&lt;/strong&gt; Individual queued jobs make the pipeline restartable, observable, and fault-tolerant. A bad file fails in isolation; everything else keeps running.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;I write about backend engineering and fintech systems at &lt;a href="https://blog.shakiltech.com" rel="noopener noreferrer"&gt;blog.shakiltech.com&lt;/a&gt;. If this was useful, the next post covers the Laravel queue architecture behind this pipeline.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>video</category>
      <category>computerscience</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Laravel Fast2SMS v2.0.0 — WhatsApp Support, Notification Channels &amp; a Smarter DX</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Sun, 29 Mar 2026 05:07:09 +0000</pubDate>
      <link>https://dev.to/itxshakil/laravel-fast2sms-v200-whatsapp-support-notification-channels-a-smarter-dx-50ik</link>
      <guid>https://dev.to/itxshakil/laravel-fast2sms-v200-whatsapp-support-notification-channels-a-smarter-dx-50ik</guid>
      <description>&lt;p&gt;When I shipped v1, the goal was simple: send SMS to Indian phone numbers from Laravel without wrestling raw API arrays. v2.0.0 takes that same idea much further.&lt;/p&gt;

&lt;p&gt;The headline is &lt;strong&gt;WhatsApp Business API support&lt;/strong&gt; — text, images, documents, locations, templates, reactions, stickers, and interactive messages, all through the same familiar &lt;code&gt;Fast2sms&lt;/code&gt; facade. Beyond that, v2 brings drop-in &lt;strong&gt;Laravel Notification Channels&lt;/strong&gt;, a &lt;strong&gt;typed exception hierarchy&lt;/strong&gt;, expressive &lt;strong&gt;testing utilities&lt;/strong&gt;, and &lt;strong&gt;cost-saving guards&lt;/strong&gt; that protect your wallet in production.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ This is a major release with breaking changes. See the &lt;a href="https://github.com/itxshakil/laravel-fast2sms/blob/main/UPGRADING.md" rel="noopener noreferrer"&gt;Upgrade Guide&lt;/a&gt; before updating.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me walk you through what matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 WhatsApp Business API
&lt;/h2&gt;

&lt;p&gt;This is the big one. Starting in v2.0.0, the package supports the Fast2SMS WhatsApp Business API through &lt;code&gt;Fast2sms::whatsapp()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Send a plain text message:&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;Shakil\Fast2sms\Facades\Fast2sms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whatsapp&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;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'9999999999'&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;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Your order has been shipped and is on its way!'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send an image with a caption:&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;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whatsapp&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;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'9999999999'&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;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://yourapp.com/invoice.png'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caption&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Your invoice for order #1042'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send a document:&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;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whatsapp&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;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'9999999999'&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;document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://yourapp.com/receipt.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'receipt.pdf'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send a location:&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;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whatsapp&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;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'9999999999'&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;location&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;28.6139&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lng&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;77.2090&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'New Delhi'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send a registered template:&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;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whatsapp&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;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'9999999999'&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;template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'YOUR_TEMPLATE_ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Order #1042'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React to a message, send a sticker, or build interactive button flows — the full WhatsApp API surface is available through the same fluent chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔔 Laravel Notification Channels
&lt;/h2&gt;

&lt;p&gt;v2 ships two drop-in notification channels: &lt;code&gt;SmsChannel&lt;/code&gt; and &lt;code&gt;WhatsAppChannel&lt;/code&gt;. Your existing notification classes work exactly the way you'd expect.&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;Shakil\Fast2sms\Channels\SmsChannel&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;Shakil\Fast2sms\Channels\WhatsAppChannel&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;Shakil\Fast2sms\Messages\SmsMessage&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;Shakil\Fast2sms\Messages\WhatsAppMessage&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;OrderShipped&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Notification&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;via&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$notifiable&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="nc"&gt;SmsChannel&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;WhatsAppChannel&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;toFast2sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$notifiable&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;SmsMessage&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SmsMessage&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;otp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Your OTP is 482910. Valid for 10 minutes.'&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;toWhatsApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$notifiable&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;WhatsAppMessage&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;WhatsAppMessage&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="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;'Your order has been shipped!'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the routing methods to your &lt;code&gt;User&lt;/code&gt; model and you're done:&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;routeNotificationForFast2sms&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;phone_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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;routeNotificationForWhatsapp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;phone_number&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;No facade, no manual number passing. Just idiomatic Laravel.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 Typed Exception Hierarchy
&lt;/h2&gt;

&lt;p&gt;In v1, every API failure threw a single &lt;code&gt;Fast2smsException&lt;/code&gt;. In v2 you can catch exactly what you need:&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;Shakil\Fast2sms\Exceptions\AuthenticationException&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;Shakil\Fast2sms\Exceptions\RateLimitException&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;Shakil\Fast2sms\Exceptions\InsufficientBalanceException&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;Shakil\Fast2sms\Exceptions\NetworkException&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="nc"&gt;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;otp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'9999999999'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'845621'&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;InsufficientBalanceException&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="c1"&gt;// Notify your team — wallet is empty&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;RateLimitException&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="c1"&gt;// Back off and retry&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;AuthenticationException&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="c1"&gt;// API key is wrong or expired&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;NetworkException&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="c1"&gt;// Fast2SMS was unreachable&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full list of typed exceptions is in the &lt;a href="https://github.com/itxshakil/laravel-fast2sms/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Rich Fake Assertions for Testing
&lt;/h2&gt;

&lt;p&gt;v2 ships 16 assertion methods on &lt;code&gt;Fast2smsFake&lt;/code&gt; so your feature tests are expressive and readable — no more inspecting raw recorded calls.&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;Fast2sms&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="c1"&gt;// ... trigger your code ...&lt;/span&gt;

&lt;span class="nc"&gt;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertSmsSentTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'9999999999'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertSmsSentTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'9999999999'&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;$sms&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;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sms&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'482910'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertWhatsAppSentTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'9999999999'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertNothingSent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Clean up for tests that need real sending&lt;/span&gt;
&lt;span class="nc"&gt;Fast2sms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stopFaking&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💡 Fluent Message Builders with Credit Helpers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SmsMessage&lt;/code&gt; and &lt;code&gt;WhatsAppMessage&lt;/code&gt; are first-class objects with named constructors and chainable setters. &lt;code&gt;SmsMessage&lt;/code&gt; also ships with credit helpers so you can estimate cost before you send:&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;Shakil\Fast2sms\Messages\SmsMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SmsMessage&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Your appointment is confirmed for tomorrow at 10am.'&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;withRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SmsRoute&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;QUICK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;charCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// character count&lt;/span&gt;
&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isUnicode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// true if Unicode encoding is needed&lt;/span&gt;
&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;creditCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// SMS credits this will consume&lt;/span&gt;
&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exceedsOneSms&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// true if longer than one SMS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more guessing whether a long message costs 1 credit or 3.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ Cost-Saving Guards
&lt;/h2&gt;

&lt;p&gt;v2 introduces a set of opt-in guards that protect your wallet in production. All are off by default — enable only what your use case needs in &lt;code&gt;config/fast2sms.php&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Guard&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recipient deduplication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strips duplicate numbers before every send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Invalid recipient stripping&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Validates numbers and logs warnings instead of failing hard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Idempotency guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Blocks the same message being sent twice within a TTL window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate throttle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sliding-window per-minute cap via Laravel cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Balance gate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Checks your wallet before sending; fires &lt;code&gt;LowBalanceDetected&lt;/code&gt; when low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch splitting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Splits large recipient lists into chunks automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ⚙️ New Artisan Commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List every event the package fires, with descriptions&lt;/span&gt;
php artisan fast2sms:events

&lt;span class="c"&gt;# Generate an IDE helper file for full autocompletion&lt;/span&gt;
php artisan fast2sms:ide-helper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;fast2sms:ide-helper&lt;/code&gt; once after install and get full autocompletion on every facade method in PhpStorm or VS Code.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Getting Started
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; PHP ^8.3 (8.4 and 8.5 also tested), Laravel 11, 12, or 13.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require itxshakil/laravel-fast2sms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fast2sms-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAST2SMS_API_KEY="your-api-key"
FAST2SMS_DEFAULT_SENDER_ID="FSTSMS"
FAST2SMS_DEFAULT_ROUTE="dlt"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Upgrading from v1
&lt;/h2&gt;

&lt;p&gt;The public sending API — &lt;code&gt;Fast2sms::quick()&lt;/code&gt;, &lt;code&gt;::dlt()&lt;/code&gt;, &lt;code&gt;::otp()&lt;/code&gt;, and now &lt;code&gt;::viaWhatsApp()&lt;/code&gt; — is &lt;strong&gt;completely unchanged&lt;/strong&gt;. Most v1 code will work without modification.&lt;/p&gt;

&lt;p&gt;The breaking changes are in internals: exception types, DTOs, and return type hints. The full step-by-step migration is in &lt;a href="https://github.com/itxshakil/laravel-fast2sms/blob/main/UPGRADING.md" rel="noopener noreferrer"&gt;UPGRADING.md&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/itxshakil/laravel-fast2sms" rel="noopener noreferrer"&gt;github.com/itxshakil/laravel-fast2sms&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Packagist:&lt;/strong&gt; &lt;a href="https://packagist.org/packages/itxshakil/laravel-fast2sms" rel="noopener noreferrer"&gt;packagist.org/packages/itxshakil/laravel-fast2sms&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📋 &lt;strong&gt;Full Changelog:&lt;/strong&gt; &lt;a href="https://github.com/itxshakil/laravel-fast2sms/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Upgrade Guide:&lt;/strong&gt; &lt;a href="https://github.com/itxshakil/laravel-fast2sms/blob/main/UPGRADING.md" rel="noopener noreferrer"&gt;UPGRADING.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Issues:&lt;/strong&gt; &lt;a href="https://github.com/itxshakil/laravel-fast2sms/issues" rel="noopener noreferrer"&gt;github.com/itxshakil/laravel-fast2sms/issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this saved you time, a star on GitHub helps other Laravel developers find the package.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://github.com/itxshakil" rel="noopener noreferrer"&gt;Shakil Alam&lt;/a&gt; — Laravel developer, open-source contributor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>sms</category>
      <category>whatsapp</category>
    </item>
    <item>
      <title>Git Explained Simply — For People Who Have No Idea What It Is</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Sun, 15 Mar 2026 08:44:59 +0000</pubDate>
      <link>https://dev.to/itxshakil/git-explained-simply-for-people-who-have-no-idea-what-it-is-53ho</link>
      <guid>https://dev.to/itxshakil/git-explained-simply-for-people-who-have-no-idea-what-it-is-53ho</guid>
      <description>&lt;p&gt;You've seen the word Git everywhere.&lt;/p&gt;

&lt;p&gt;Job listings. GitHub. Tutorials that just assume you already know what it means. Nobody ever stopped to explain it from scratch.&lt;/p&gt;

&lt;p&gt;This post is that explanation.&lt;/p&gt;

&lt;p&gt;No jargon. No assumed knowledge. By the end you'll know what Git is, why it exists, and how to make your first commit — in about 15 minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Git?
&lt;/h2&gt;

&lt;p&gt;Git is a tool that &lt;strong&gt;saves every version of your code&lt;/strong&gt; — not just the latest one.&lt;/p&gt;

&lt;p&gt;Here's a simple way to think about it:&lt;/p&gt;

&lt;p&gt;Imagine you're writing an essay in Word. Every time you hit Save, the old version disappears. You only ever have the latest copy. Delete a paragraph and save — that paragraph is gone forever.&lt;/p&gt;

&lt;p&gt;Now imagine you could hit a special kind of Save that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeps every previous version, forever&lt;/li&gt;
&lt;li&gt;Lets you go back to any version at any time&lt;/li&gt;
&lt;li&gt;Lets you add a note explaining what you changed and why&lt;/li&gt;
&lt;li&gt;Lets you try something risky without touching your working version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's Git. For code instead of essays.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does this matter?
&lt;/h2&gt;

&lt;p&gt;Without Git, developers end up doing things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Saving files as &lt;code&gt;project_final.js&lt;/code&gt;, &lt;code&gt;project_final_v2.js&lt;/code&gt;, &lt;code&gt;project_ACTUAL_FINAL.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Deleting something, realising they needed it, having no way to get it back&lt;/li&gt;
&lt;li&gt;Two people editing the same file and overwriting each other's work&lt;/li&gt;
&lt;li&gt;Being afraid to make changes because they might break something&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Git solves all of these. That's why every professional developer uses it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Git vs GitHub — they are not the same thing
&lt;/h2&gt;

&lt;p&gt;This trips up almost everyone at first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git&lt;/strong&gt; is a tool that runs on your computer. It tracks your code history. It's free, open source, and works completely offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt; is a website. It stores your Git history online, lets you share your code, and makes it easy to collaborate with other developers. Think of it like Google Drive, but built specifically for Git.&lt;/p&gt;

&lt;p&gt;You can use Git without GitHub. But most developers use both together — Git on their machine to track changes, GitHub online to back up and share.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five words you'll hear constantly
&lt;/h2&gt;

&lt;p&gt;Before we touch any commands, here are the five terms that come up in almost every Git conversation. Read them once — you don't need to memorise them, just have a rough idea so they don't throw you when you see them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository (repo)&lt;/strong&gt;&lt;br&gt;
A repository is just a project folder that Git is watching. That's it. When a developer says "clone the repo" or "push to the repo" they mean the project. Your &lt;code&gt;my-first-project&lt;/code&gt; folder? Once you run &lt;code&gt;git init&lt;/code&gt; inside it, that's a repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit&lt;/strong&gt;&lt;br&gt;
A commit is a snapshot — a permanent record of exactly what your code looked like at a specific moment. Every commit has a short message you write yourself, like &lt;code&gt;"Add login page"&lt;/code&gt; or &lt;code&gt;"Fix broken checkout button"&lt;/code&gt;. Think of it as pressing Save, but instead of overwriting the last save, Git keeps every single one and lines them up in order. You can go back to any of them at any time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch&lt;/strong&gt;&lt;br&gt;
Imagine you're writing a book and you want to try a completely different ending — but you don't want to delete the original. You'd make a copy, experiment on the copy, and if it works, swap it in. If it doesn't, throw it away.&lt;/p&gt;

&lt;p&gt;That's a branch. It's a separate version of your project where you can make changes freely, without touching anything that's working. When you're happy with the changes, you merge the branch back in. If you hate it, you just delete the branch — your original code is completely untouched.&lt;/p&gt;

&lt;p&gt;This is one of the most powerful things Git does, and you'll use it constantly once you get the hang of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push&lt;/strong&gt;&lt;br&gt;
When you commit, the snapshot is saved on &lt;em&gt;your computer only&lt;/em&gt;. Push is how you send those commits up to GitHub so they're backed up online and others can see them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Computer → GitHub = Push&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Pull&lt;/strong&gt;&lt;br&gt;
The opposite of push. If someone else on your team made changes and pushed them to GitHub, pull is how you get those changes down onto your computer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GitHub → Computer = Pull&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's all five. After you've used Git for a day or two, these words will feel completely natural.&lt;/p&gt;


&lt;h2&gt;
  
  
  Your first 15 minutes with Git
&lt;/h2&gt;

&lt;p&gt;Let's actually do it rather than just talk about it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1 — Install Git
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;&lt;a href="https://git-scm.com" rel="noopener noreferrer"&gt;git-scm.com&lt;/a&gt;&lt;/strong&gt; and download Git for your system. Install with the default options.&lt;/p&gt;

&lt;p&gt;Open your terminal and check it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see something like &lt;code&gt;git version 2.43.0&lt;/code&gt; — you're set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Tell Git who you are
&lt;/h3&gt;

&lt;p&gt;Git labels every commit with your name and email. Set them once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.name &lt;span class="s2"&gt;"Your Name"&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.email &lt;span class="s2"&gt;"you@example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This only needs doing once per computer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Create your first repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-first-project
&lt;span class="nb"&gt;cd &lt;/span&gt;my-first-project
git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git init&lt;/code&gt; tells Git: &lt;em&gt;start watching this folder.&lt;/em&gt; That's it — you now have a Git repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Add a file and make your first commit
&lt;/h3&gt;

&lt;p&gt;Create a simple file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello, Git!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hello.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saving a file in Git is a two-step process:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage it&lt;/strong&gt; — tell Git you want to include this file in the next snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add hello.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Commit it&lt;/strong&gt; — take the actual snapshot, with a message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add hello.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You just made your first commit. Git has permanently recorded this moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 — Make a change and watch Git track it
&lt;/h3&gt;

&lt;p&gt;Edit &lt;code&gt;hello.txt&lt;/code&gt; — change the text to anything, save the file. Now run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git tells you the file has changed. To see exactly &lt;em&gt;what&lt;/em&gt; changed, line by line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lines you removed show in red. Lines you added show in green. Git caught everything automatically.&lt;/p&gt;

&lt;p&gt;Commit the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add hello.txt
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Update the greeting"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6 — Look at your history
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both commits are listed. This list grows with every commit you ever make. It never gets deleted. That's your permanent history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why two steps? (The staging question everyone asks)
&lt;/h2&gt;

&lt;p&gt;New developers always wonder why you can't just commit directly without staging first.&lt;/p&gt;

&lt;p&gt;The staging step (&lt;code&gt;git add&lt;/code&gt;) exists so you can choose &lt;em&gt;exactly&lt;/em&gt; what goes into a commit. You might have changed five files but only want to save two of them right now. Staging gives you that precision.&lt;/p&gt;

&lt;p&gt;It feels awkward the first few times. After a week it becomes automatic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one habit that makes all the difference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Commit small. Commit often.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every time you finish one thing — not everything, just one small thing — make a commit. Every time the code is in a working state you want to remember, make a commit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;❌ Bad commit message&lt;/th&gt;
&lt;th&gt;✅ Good commit message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stuff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Add email validation to the signup form&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fix&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Fix broken checkout button on mobile&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Update user profile to allow avatar uploads&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The message is not for Git. It's for you, six months from now, trying to figure out what you were thinking.&lt;/p&gt;

&lt;p&gt;Most teams follow a format called Conventional Commits that makes messages consistent across the whole team. It's the single most impactful habit to build early: &lt;strong&gt;&lt;a href="https://blog.shakiltech.com/conventional-commits/" rel="noopener noreferrer"&gt;Commit Like a Pro: A Beginner's Guide to Conventional Commits&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What to learn next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Set up Git properly for real projects&lt;/strong&gt;&lt;br&gt;
SSH keys, useful config, connecting to GitHub — the setup that makes Git work smoothly day to day:&lt;br&gt;
&lt;a href="https://blog.shakiltech.com/practical-github-setup/" rel="noopener noreferrer"&gt;Practical Git &amp;amp; GitHub Setup for Real-World Projects&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;git switch vs git checkout&lt;/strong&gt;&lt;br&gt;
You'll see both in tutorials. They do similar things — here's the difference:&lt;br&gt;
&lt;a href="https://blog.shakiltech.com/git-checkout-vs-git-switch/" rel="noopener noreferrer"&gt;Git Checkout vs Git Switch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to recover anything you thought you deleted&lt;/strong&gt;&lt;br&gt;
Once you've used Git for a bit, this is the one that makes you feel invincible:&lt;br&gt;
&lt;a href="https://blog.shakiltech.com/git-reflog-explained-recover-deleted-commits-lost-work/" rel="noopener noreferrer"&gt;Git Reflog Explained: Recover Deleted Commits &amp;amp; Lost Work&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find exactly which commit broke something&lt;/strong&gt;&lt;br&gt;
Git has a built-in tool that searches your entire history automatically:&lt;br&gt;
&lt;a href="https://blog.shakiltech.com/git-recovery-commands/" rel="noopener noreferrer"&gt;Git as Your Safety Net: The Confidence to Work Fearlessly&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The full Git Mastery Series
&lt;/h2&gt;

&lt;p&gt;When you're ready to go beyond the basics, this series takes you from how Git actually thinks to using it as your permanent safety net:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://blog.shakiltech.com/how-git-thinks/" rel="noopener noreferrer"&gt;How Git Actually Thinks&lt;/a&gt; — the mental model that changes everything&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.shakiltech.com/git-commit-best-practices/" rel="noopener noreferrer"&gt;Committing with Intention&lt;/a&gt; — writing commits that mean something&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.shakiltech.com/git-branching-strategy/" rel="noopener noreferrer"&gt;Branching Without Fear&lt;/a&gt; — working in parallel without breaking things&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.shakiltech.com/git-collaboration-workflow/" rel="noopener noreferrer"&gt;Collaboration That Doesn't Create Chaos&lt;/a&gt; — working with a team&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.shakiltech.com/git-recovery-commands/" rel="noopener noreferrer"&gt;Git as Your Safety Net&lt;/a&gt; — recovering from anything&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want everything in one place — checklists, 80+ commands, hook templates, and a recovery playbook for when things go wrong — I put it all into a 23-page PDF you can keep open while you work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://itxshakil.gumroad.com/l/git-mastery" rel="noopener noreferrer"&gt;Git Mastery Field Guide →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But start here. Get the basics under your fingers first. Everything else builds on top of this.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions? Drop them in the comments. There are no silly questions when you're just getting started.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>github</category>
    </item>
    <item>
      <title>Git as Your Safety Net: The Confidence to Work Fearlessly</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Sun, 15 Mar 2026 08:29:50 +0000</pubDate>
      <link>https://dev.to/itxshakil/git-as-your-safety-net-the-confidence-to-work-fearlessly-4k86</link>
      <guid>https://dev.to/itxshakil/git-as-your-safety-net-the-confidence-to-work-fearlessly-4k86</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 5 of the Git Mastery Series&lt;br&gt;
← &lt;a href="https://dev.to/itxshakil/collaboration-that-doesnt-create-chaos-4k45"&gt;Part 4: Collaboration That Doesn't Create Chaos&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By the time most developers reach this level of Git knowledge, they've already had the experience that makes everything click.&lt;/p&gt;

&lt;p&gt;Maybe they ran &lt;code&gt;git reset --hard&lt;/code&gt; and thought they lost a day's work. Maybe they deleted a branch too early. Maybe a rebase went sideways and they couldn't figure out how to get back to where they started. Maybe they pushed directly to main and spent an hour trying to undo it.&lt;/p&gt;

&lt;p&gt;Those moments are terrifying when you don't know about Git's safety nets. Once you do, they become inconveniences — things you recover from in five minutes rather than crises.&lt;/p&gt;

&lt;p&gt;This part isn't about new commands to memorize. It's about changing your relationship with Git entirely — from treating it as something to be careful around to using it as the thing that makes you &lt;em&gt;less&lt;/em&gt; careful, because you know you can recover from almost anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Nothing Is Permanent Until the Garbage Collector Runs
&lt;/h2&gt;

&lt;p&gt;Here's the most liberating thing to understand about Git: deleting a commit doesn't delete it.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;git reset --hard HEAD~3&lt;/code&gt;, you're moving the branch pointer backward three commits. The three commits you "deleted" are still in the object store. They still exist as objects in the &lt;code&gt;.git&lt;/code&gt; folder. They're just not pointed to by any branch anymore.&lt;/p&gt;

&lt;p&gt;Git's garbage collector (&lt;code&gt;git gc&lt;/code&gt;) runs automatically every few hundred operations or when you explicitly call it. By default, it only removes objects that have been unreferenced for more than 90 days. You have a 90-day window to recover almost anything.&lt;/p&gt;

&lt;p&gt;This means: &lt;strong&gt;you can be bold&lt;/strong&gt;. Try risky rebases. Reset commits. Delete branches. Move things around. The cost of being wrong is usually five minutes of recovery, not lost work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reflog: Git's Black Box Recorder
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git reflog&lt;/code&gt; is the command that has saved more careers than any other.&lt;/p&gt;

&lt;p&gt;Every time HEAD moves — every commit, checkout, rebase, reset, merge, cherry-pick — Git records it in the reflog. This is completely separate from your commit history. It's a log of every position HEAD has ever been at, including states that no longer have a branch pointing to them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reflog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a3f8c9d HEAD@{0}: commit: fix(auth): handle null token on refresh
1b4e7a2 HEAD@{1}: rebase (finish): returning to refs/heads/feature/auth
9c4d2e1 HEAD@{2}: rebase (pick): feat(auth): add OTP verification
7a5b3f8 HEAD@{3}: rebase (pick): feat(auth): add login endpoint  
2e8d1c4 HEAD@{4}: checkout: moving from main to feature/auth
f3a9b7e HEAD@{5}: reset: moving to HEAD~3
8d2c5a1 HEAD@{6}: commit: add debug logging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that &lt;code&gt;HEAD@{5}&lt;/code&gt; — a &lt;code&gt;reset: moving to HEAD~3&lt;/code&gt;? Those three commits you "deleted" are still accessible. &lt;code&gt;HEAD@{6}&lt;/code&gt; is one of them. You can get back to that state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Recover from that reset&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD@&lt;span class="o"&gt;{&lt;/span&gt;6&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Or create a branch from that lost commit&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; recovery/lost-work HEAD@&lt;span class="o"&gt;{&lt;/span&gt;6&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Scenario: you deleted a branch before merging&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find where the branch was&lt;/span&gt;
git reflog | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"branch-name"&lt;/span&gt;
&lt;span class="c"&gt;# or just scroll through and find the last commit on it&lt;/span&gt;

&lt;span class="c"&gt;# Create a new branch at that commit&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/recovered-branch a3f8c9d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Scenario: rebase went wrong&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before rebasing, note where you are&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;span class="c"&gt;# a3f8c9d feat: current state&lt;/span&gt;

&lt;span class="c"&gt;# Rebase goes badly&lt;/span&gt;
git rebase main
&lt;span class="c"&gt;# This looks completely wrong...&lt;/span&gt;

&lt;span class="c"&gt;# Go back to before the rebase started&lt;/span&gt;
git reflog
&lt;span class="c"&gt;# Find the entry just before "rebase (start)"&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD@&lt;span class="o"&gt;{&lt;/span&gt;N&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reflog only exists locally — it's not pushed to remote. So it won't help you recover something on another developer's machine. But for your own work, it's a complete audit trail.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Commands That Feel Dangerous (And What They Actually Do)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;git reset --hard&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one developers fear most. It moves the branch pointer and updates both the staging area and working directory to match the target commit. Any uncommitted changes are gone.&lt;/p&gt;

&lt;p&gt;The key word: uncommitted. If your changes are committed, &lt;code&gt;reset --hard&lt;/code&gt; doesn't lose them — it just makes them harder to find. Use &lt;code&gt;git reflog&lt;/code&gt; to find the commit hash, then &lt;code&gt;git reset --hard &amp;lt;that-hash&amp;gt;&lt;/code&gt; to get back.&lt;/p&gt;

&lt;p&gt;If you had &lt;em&gt;uncommitted&lt;/em&gt; changes, those are genuinely gone from Git's perspective. But your editor's local history (VS Code's timeline, IntelliJ's local history) often has them. Check there first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;git rebase&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rebase feels risky because it rewrites commit hashes. But here's what actually happens: Git creates &lt;em&gt;new&lt;/em&gt; commits with the same changes but different parent hashes. The old commits still exist. Before any significant rebase, the reflog has your back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# If rebase goes wrong, find where you were before it started&lt;/span&gt;
git reflog | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;span class="c"&gt;# Find the "rebase (start)" entry and look one above it&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD@&lt;span class="o"&gt;{&lt;/span&gt;N&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;git push --force&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is actually dangerous on shared branches — you genuinely can overwrite someone else's work. But on your own feature branch? It's routine after an interactive rebase. The safe variant &lt;code&gt;--force-with-lease&lt;/code&gt; checks that nobody else has pushed since you last fetched before allowing the force push.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;git clean -fd&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This removes untracked files and directories. Unlike most Git operations, this is genuinely irreversible — untracked files aren't in Git's object store, so they can't be recovered with reflog. Always run &lt;code&gt;git clean -nd&lt;/code&gt; first (the dry run) to see exactly what would be deleted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clean &lt;span class="nt"&gt;-nd&lt;/span&gt;  &lt;span class="c"&gt;# dry run — shows what would be deleted&lt;/span&gt;
git clean &lt;span class="nt"&gt;-fd&lt;/span&gt;  &lt;span class="c"&gt;# actually delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Stash: The Scratch Pad Between Branches
&lt;/h2&gt;

&lt;p&gt;You're halfway through a feature when someone says there's a production bug that needs fixing immediately. Your working directory has changes everywhere. You don't want to commit half-finished work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Save current work&lt;/span&gt;
git stash

&lt;span class="c"&gt;# Switch to main, fix the bug, push&lt;/span&gt;
git switch main
&lt;span class="c"&gt;# ... fix bug ...&lt;/span&gt;
git switch feature/my-feature

&lt;span class="c"&gt;# Get your work back&lt;/span&gt;
git stash pop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stash saves your working directory and staging area state, and gives you a clean working directory. &lt;code&gt;git stash pop&lt;/code&gt; restores it and removes the stash entry.&lt;/p&gt;

&lt;p&gt;A few things most developers don't know about stash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Give the stash a meaningful name (you'll thank yourself later)&lt;/span&gt;
git stash push &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"halfway through payment refactor"&lt;/span&gt;

&lt;span class="c"&gt;# See all stashes&lt;/span&gt;
git stash list
&lt;span class="c"&gt;# stash@{0}: On feature/auth: halfway through payment refactor&lt;/span&gt;
&lt;span class="c"&gt;# stash@{1}: On main: quick experiment&lt;/span&gt;

&lt;span class="c"&gt;# Apply a specific stash (without removing it)&lt;/span&gt;
git stash apply stash@&lt;span class="o"&gt;{&lt;/span&gt;1&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Stash only specific files&lt;/span&gt;
git stash push &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"auth changes only"&lt;/span&gt; src/auth/

&lt;span class="c"&gt;# Stash including untracked files&lt;/span&gt;
git stash &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One warning: stashes are local. They're not pushed to remote. If you stash something on your laptop and switch to another machine, that stash doesn't exist there. Don't use stash as a long-term storage mechanism — it's a scratch pad, not a parking lot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cherry-Pick: Taking Exactly What You Need
&lt;/h2&gt;

&lt;p&gt;Sometimes a commit exists on one branch and you need it on another, without merging the whole branch.&lt;/p&gt;

&lt;p&gt;You fixed a critical bug on &lt;code&gt;feature/auth&lt;/code&gt; but it's not merged yet. You need that fix on &lt;code&gt;main&lt;/code&gt; today. Cherry-pick applies the exact changes from that commit onto your current branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get the commit hash&lt;/span&gt;
git log feature/auth &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;span class="c"&gt;# a3f8c9d fix(auth): prevent session fixation on login&lt;/span&gt;

&lt;span class="c"&gt;# Apply it to main&lt;/span&gt;
git switch main
git cherry-pick a3f8c9d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git creates a new commit on &lt;code&gt;main&lt;/code&gt; with the same changes but a different hash. The original commit on &lt;code&gt;feature/auth&lt;/code&gt; is unchanged.&lt;/p&gt;

&lt;p&gt;Cherry-pick is precise and powerful, but use it intentionally. If you find yourself cherry-picking many commits between branches regularly, that's usually a sign your branching strategy has a gap — there should be a path to get those changes merged properly rather than manually copied.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bisect: Binary Search for the Bug That Snuck In
&lt;/h2&gt;

&lt;p&gt;Something is broken. You don't know which commit introduced it. You know it worked last week. There might be 200 commits between "worked" and "broken."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git bisect&lt;/code&gt; runs a binary search through your history, cutting the search space in half at each step. 200 commits becomes 7–8 tests instead of 200.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git bisect start
git bisect bad                     &lt;span class="c"&gt;# Current state is broken&lt;/span&gt;
git bisect good v2.1.0             &lt;span class="c"&gt;# This tag was working&lt;/span&gt;

&lt;span class="c"&gt;# Git checks out the midpoint commit&lt;/span&gt;
&lt;span class="c"&gt;# Test your application...&lt;/span&gt;
&lt;span class="c"&gt;# Working? &lt;/span&gt;
git bisect good
&lt;span class="c"&gt;# Broken?&lt;/span&gt;
git bisect bad

&lt;span class="c"&gt;# After 7-8 steps, Git identifies the exact commit:&lt;/span&gt;
&lt;span class="c"&gt;# a3f8c9d is the first bad commit&lt;/span&gt;

git bisect reset  &lt;span class="c"&gt;# Return to original state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have a test that can verify the bug automatically, bisect becomes even more powerful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git bisect run php artisan &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UserAuthTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git checks out each midpoint, runs the test, and classifies it automatically. You can walk away and come back to the answer. This is how you debug a regression that crept in across a long sprint — not by reading every commit, but by letting Git find it for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Worktrees: Multiple Branches at Once Without Stashing
&lt;/h2&gt;

&lt;p&gt;Here's a scenario: you're deep into a feature branch and need to check something on &lt;code&gt;main&lt;/code&gt; — not just look at a file, but actually run the code. Normally you'd stash, switch, run, switch back, pop stash.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt; lets you check out a second branch into a separate folder, running both simultaneously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check out main into a separate folder&lt;/span&gt;
git worktree add ../project-main main

&lt;span class="c"&gt;# Now you have:&lt;/span&gt;
&lt;span class="c"&gt;# /project              (your feature branch)&lt;/span&gt;
&lt;span class="c"&gt;# /project-main         (main branch, fully checked out)&lt;/span&gt;

&lt;span class="c"&gt;# Work in both simultaneously&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../project-main
php artisan serve &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8001

&lt;span class="c"&gt;# When done, remove the worktree&lt;/span&gt;
git worktree remove ../project-main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each worktree shares the same &lt;code&gt;.git&lt;/code&gt; folder — they're not separate repositories. Changes committed in one worktree are immediately visible in the other. This is particularly useful for reviewing a colleague's PR while staying on your own branch, or comparing behavior between branches in real-time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building a Git Workflow You Actually Trust
&lt;/h2&gt;

&lt;p&gt;The developers who use Git most confidently aren't the ones who know the most commands. They're the ones who've built habits that mean they rarely need recovery procedures in the first place.&lt;/p&gt;

&lt;p&gt;A few habits that, combined, create a workflow with almost no risk:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit often.&lt;/strong&gt; Small, atomic commits are easier to revert, cherry-pick, and understand. A commit every 30–60 minutes of real work is not too often. It's exactly right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch before you start anything.&lt;/strong&gt; Ten seconds. Every time. The habit that makes &lt;code&gt;main&lt;/code&gt; always deployable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push your branches regularly.&lt;/strong&gt; A branch that only exists on your laptop is lost if your laptop dies. Push feature branches daily even if they're not ready to merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check &lt;code&gt;git status&lt;/code&gt; before destructive operations.&lt;/strong&gt; Before &lt;code&gt;reset --hard&lt;/code&gt;, &lt;code&gt;clean -fd&lt;/code&gt;, or anything that might lose uncommitted changes, run &lt;code&gt;git status&lt;/code&gt;. Take two seconds to read what you'd be throwing away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learn &lt;code&gt;git reflog&lt;/code&gt;'s address by heart.&lt;/strong&gt; When something goes wrong, &lt;code&gt;git reflog&lt;/code&gt; is almost always the first thing to check. It should be a reflex.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Freedom That Comes From Understanding
&lt;/h2&gt;

&lt;p&gt;The relationship most developers have with Git is one of cautious navigation — running commands that seem to work, avoiding the ones that seem dangerous, hoping nothing goes wrong.&lt;/p&gt;

&lt;p&gt;The relationship you can have with Git is entirely different: active, confident, fearless. You run experiments knowing you can undo them. You rebase complex histories knowing you can restore the original with reflog. You delete branches knowing you can recover them from the object store. You reset commits knowing you can find them again.&lt;/p&gt;

&lt;p&gt;That confidence changes how you work. You try bolder refactors because you know you can undo them. You explore unfamiliar parts of a codebase more freely because you can revert your exploration. You commit half-finished thoughts because you'll clean them up before the PR.&lt;/p&gt;

&lt;p&gt;Git was built to make working with code feel safe. Once you understand it — really understand it, not just the commands but the model — that's exactly what it feels like.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Complete Recovery Toolkit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See every position HEAD has been at&lt;/span&gt;
git reflog

&lt;span class="c"&gt;# Recover from a bad reset&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD@&lt;span class="o"&gt;{&lt;/span&gt;N&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Recover a deleted branch&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; recovered-branch &amp;lt;hash-from-reflog&amp;gt;

&lt;span class="c"&gt;# Undo a commit on shared branch (safe)&lt;/span&gt;
git revert HEAD

&lt;span class="c"&gt;# Stash and restore work in progress&lt;/span&gt;
git stash push &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"description"&lt;/span&gt;
git stash pop

&lt;span class="c"&gt;# Cherry-pick a specific commit&lt;/span&gt;
git cherry-pick &amp;lt;commit-hash&amp;gt;

&lt;span class="c"&gt;# Binary search for a breaking commit&lt;/span&gt;
git bisect start
git bisect bad
git bisect good &amp;lt;known-good-hash&amp;gt;
git bisect run &amp;lt;test-command&amp;gt;
git bisect reset

&lt;span class="c"&gt;# Run two branches simultaneously&lt;/span&gt;
git worktree add ../folder branch-name
git worktree remove ../folder

&lt;span class="c"&gt;# Dry run before destructive operations&lt;/span&gt;
git clean &lt;span class="nt"&gt;-nd&lt;/span&gt;   &lt;span class="c"&gt;# Preview what clean would delete&lt;/span&gt;
git reset &lt;span class="nt"&gt;--dry-run&lt;/span&gt;  &lt;span class="c"&gt;# Not all Git versions support this; use status instead&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;← &lt;a href="https://dev.to/itxshakil/collaboration-that-doesnt-create-chaos-4k45"&gt;Part 4: Collaboration That Doesn't Create Chaos&lt;/a&gt; | &lt;a href="https://dev.to/itxshakil/git-mastery-the-complete-series-4h5b"&gt;Back to the Series Hub →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;If this was useful, I turned the whole series into a 23-page PDF reference — checklists, hook templates, 80+ commands, reflog &amp;amp; bisect deep-dives, and a recovery playbook for 12 real emergencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://itxshakil.gumroad.com/l/git-mastery" rel="noopener noreferrer"&gt;Git Mastery Field Guide →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If this was useful, I turned the whole series into a 23-page PDF reference — checklists, hook templates, 80+ commands, reflog &amp;amp; bisect deep-dives, and a recovery playbook for 12 real emergencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://itxshakil.gumroad.com/l/git-mastery" rel="noopener noreferrer"&gt;Git Mastery Field Guide →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>git</category>
      <category>github</category>
    </item>
    <item>
      <title>Collaboration That Doesn't Create Chaos</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Sun, 15 Mar 2026 08:27:38 +0000</pubDate>
      <link>https://dev.to/itxshakil/collaboration-that-doesnt-create-chaos-4k45</link>
      <guid>https://dev.to/itxshakil/collaboration-that-doesnt-create-chaos-4k45</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 4 of the Git Mastery Series&lt;br&gt;
← &lt;a href="https://dev.to/itxshakil/branching-without-fear-65l"&gt;Part 3: Branching Without Fear&lt;/a&gt; | &lt;a href="https://dev.to/itxshakil/git-as-your-safety-net-the-confidence-to-work-fearlessly-4k86"&gt;Part 5: Git as Your Safety Net →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first time you work on a shared repository with a team, Git feels entirely different. Suddenly the stakes are higher. You're not just managing your own work — you're touching code other people depend on, potentially rewriting history they've already pulled, or introducing changes that conflict with what someone else spent the day building.&lt;/p&gt;

&lt;p&gt;Most Git problems on teams aren't technical. They're coordination problems that Git reflects back at you. The branch with 47 commits that's been open for three weeks. The merge that had 12 conflicts because two developers refactored the same file in parallel without knowing it. The force push that rewrote history on a shared branch and caused chaos for the rest of the team.&lt;/p&gt;

&lt;p&gt;These situations all had a Git solution. But the solution was mostly about communication and process, not commands.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pull Request Isn't a Formality
&lt;/h2&gt;

&lt;p&gt;A lot of teams treat pull requests as a checkbox — open it, someone clicks "approve," merge it. The code review is cursory. Feedback is minimal. "LGTM" appears within minutes on PRs that contain hundreds of lines of changes.&lt;/p&gt;

&lt;p&gt;This is a missed opportunity. A pull request is the best point in the entire development process to catch problems — not just bugs, but design issues, architectural drift, security vulnerabilities, and code that technically works but will be unmaintainable in six months.&lt;/p&gt;

&lt;p&gt;The problem is that most PRs aren't designed to be reviewed. They're designed to be merged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a reviewable PR looks like:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The title is specific: &lt;code&gt;feat(checkout): add Razorpay payment gateway integration&lt;/code&gt; not &lt;code&gt;payment stuff&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's a description that answers: what does this PR do, why was it needed, how should the reviewer test it, and is there anything specific you want feedback on?&lt;/p&gt;

&lt;p&gt;The commits tell the story of what was built — not just what you happened to type while building it. (This is why Part 2 matters.)&lt;/p&gt;

&lt;p&gt;The PR is a size a human can actually review in 30–60 minutes. If it's 2,000 lines of diff, it's not a PR — it's a project. Break it up.&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;## What does this PR do?&lt;/span&gt;
Integrates Razorpay as a payment gateway option for checkout.
Previously we only supported Stripe.

&lt;span class="gu"&gt;## Why?&lt;/span&gt;
Multiple users in India reported Stripe charges in USD are
causing currency conversion frustration. Razorpay handles INR natively.

&lt;span class="gu"&gt;## How to test&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Set RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET in .env (test keys in 1Password)
&lt;span class="p"&gt;2.&lt;/span&gt; Add an item to cart and proceed to checkout
&lt;span class="p"&gt;3.&lt;/span&gt; Select "Pay with Razorpay" 
&lt;span class="p"&gt;4.&lt;/span&gt; Use test card: 4111 1111 1111 1111

&lt;span class="gu"&gt;## Notes for reviewer&lt;/span&gt;
The webhook handling in PaymentController is the most complex part.
I'd appreciate extra eyes on lines 87-134.

Closes #312
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That takes five minutes to write and saves the reviewer twenty minutes of confusion. It also forces &lt;em&gt;you&lt;/em&gt; to think about whether what you built is actually what was needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping Your Branch Current Without Creating a Mess
&lt;/h2&gt;

&lt;p&gt;The longer your feature branch lives, the further it drifts from &lt;code&gt;main&lt;/code&gt;. Merge conflicts get worse the longer you wait. Changes other people made become harder to reconcile.&lt;/p&gt;

&lt;p&gt;The habit: update your branch from &lt;code&gt;main&lt;/code&gt; regularly. Every day, or whenever something relevant lands on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two ways to do this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rebase onto main&lt;/strong&gt; (preferred for feature branches):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch feature/my-feature
git fetch origin
git rebase origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your commits get replayed on top of the latest &lt;code&gt;main&lt;/code&gt;. Your branch history stays clean. When you eventually merge, it's a fast-forward. No merge commit, no noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merge main into your branch&lt;/strong&gt; (simpler but messier):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch feature/my-feature
git merge main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a merge commit on your feature branch every time you do it. If you update three times before merging, you have three merge commits on a feature branch. The history becomes confusing. Prefer rebase for this specific operation.&lt;/p&gt;

&lt;p&gt;One important rule: if your branch is shared with other developers — if someone else has pulled it and is working from it — don't rebase. Rebasing rewrites commit hashes, which means anyone who has your old commits now has conflicts with the new ones. On shared branches, merge instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Protected Branches and Why "No Direct Push to Main" Is a Feature
&lt;/h2&gt;

&lt;p&gt;Most teams eventually learn this lesson the hard way: a direct push to &lt;code&gt;main&lt;/code&gt; that breaks the build on a Friday afternoon.&lt;/p&gt;

&lt;p&gt;Branch protection rules exist to make mistakes hard by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Require pull requests before merging&lt;/li&gt;
&lt;li&gt;Require at least one approval&lt;/li&gt;
&lt;li&gt;Require CI to pass before merging&lt;/li&gt;
&lt;li&gt;Prevent force pushes on main&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setting these up on GitHub or GitLab takes five minutes and prevents a category of incidents that are otherwise entirely predictable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings → Branches → Branch protection rules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mindset here: &lt;strong&gt;protection rules aren't about distrust&lt;/strong&gt;. They're about creating a system where good practices are automatic and mistakes require deliberate effort to make. A team that needs protection rules isn't a bad team — it's a team that understands that humans make mistakes under pressure and has designed for that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Force Push: When It's Right and When It's Wrong
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git push --force&lt;/code&gt; has a reputation as a dangerous command, and it deserves it in the wrong context. But in the right context, it's completely normal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When force push is right:&lt;/strong&gt; after rebasing or amending commits on your own feature branch that nobody else has pulled. You've rewritten local history and need to update the remote to match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rebase &lt;span class="nt"&gt;-i&lt;/span&gt; HEAD~3  &lt;span class="c"&gt;# Clean up commits&lt;/span&gt;
git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt; origin feature/my-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;--force-with-lease&lt;/code&gt; instead of &lt;code&gt;--force&lt;/code&gt;. This is strictly safer — it refuses to force push if someone else has pushed to the branch since you last fetched. It protects against accidentally overwriting someone else's work on a shared branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When force push is wrong:&lt;/strong&gt; on &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;develop&lt;/code&gt;, or any branch other people are working from. Rewriting shared history is how you get frantic Slack messages and conflicts everyone has to manually resolve.&lt;/p&gt;

&lt;p&gt;Simple rule: force push only on branches you own. Never on shared branches. If you've accidentally pushed something to a shared branch that needs to be removed — bad credentials, sensitive data, large binary files — have a team conversation before force pushing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Git Hooks: Automation That Enforces Standards
&lt;/h2&gt;

&lt;p&gt;Git hooks are scripts that run automatically at specific points in the Git workflow. They're one of the most powerful and underused features for teams.&lt;/p&gt;

&lt;p&gt;Useful hooks for shared work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pre-commit&lt;/strong&gt;: runs before a commit is finalized. Use it to run linters, check for debug statements, validate commit message format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# .git/hooks/pre-commit&lt;/span&gt;
npm run lint
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Linting failed. Fix errors before committing."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;commit-msg&lt;/strong&gt;: validates the commit message format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# .git/hooks/commit-msg&lt;/span&gt;
&lt;span class="nv"&gt;commit_regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'^(feat|fix|refactor|docs|test|chore|perf)(\(.+\))?: .{1,72}'&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$commit_regex&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Commit message doesn't follow Conventional Commits format."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Example: feat(auth): add OTP login"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;pre-push&lt;/strong&gt;: runs before pushing to remote. Good for running tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# .git/hooks/pre-push&lt;/span&gt;
npm &lt;span class="nb"&gt;test
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tests failed. Fix them before pushing."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem with &lt;code&gt;.git/hooks&lt;/code&gt; is that it's not committed to the repository — every developer has to set it up manually. For shared hooks, use a tool like &lt;strong&gt;Husky&lt;/strong&gt; (JavaScript projects) or &lt;strong&gt;Lefthook&lt;/strong&gt; (language-agnostic) that stores hooks in the repo and installs them automatically.&lt;/p&gt;

&lt;p&gt;The mindset: hooks remove the "I forgot" category of mistakes. Nobody remembers to run the linter before every commit. A hook that does it automatically means the standard is enforced without depending on anyone's memory.&lt;/p&gt;




&lt;h2&gt;
  
  
  When History Goes Wrong on a Shared Branch
&lt;/h2&gt;

&lt;p&gt;Sometimes, despite good practices, something bad lands on a shared branch. A commit with credentials. A merge that introduced a regression. A massive binary file that shouldn't have been committed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reverting vs resetting:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt; creates a new commit that undoes the changes of a previous commit. It doesn't rewrite history — it adds to it. This is the safe option for shared branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Undo the last commit while preserving history&lt;/span&gt;
git revert HEAD
git push origin main

&lt;span class="c"&gt;# Undo a specific commit further back&lt;/span&gt;
git revert a3f8c9d
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt; moves the branch pointer backward, effectively removing commits from the visible history. This is only safe on branches you haven't shared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Undo last commit (keep changes in working dir)&lt;/span&gt;
git reset HEAD~1

&lt;span class="c"&gt;# Undo last commit completely&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD~1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule: on shared branches, revert. On private branches, reset.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tagging: The Commit History Entry Nobody Writes
&lt;/h2&gt;

&lt;p&gt;Tags are a lightweight way to mark important points in history — releases, milestones, deployment checkpoints. Most teams skip them. The teams that use them find them invaluable for production debugging.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a release tag&lt;/span&gt;
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; v1.2.0 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Release version 1.2.0"&lt;/span&gt;
git push origin v1.2.0

&lt;span class="c"&gt;# Push all tags&lt;/span&gt;
git push origin &lt;span class="nt"&gt;--tags&lt;/span&gt;

&lt;span class="c"&gt;# See all tags&lt;/span&gt;
git tag

&lt;span class="c"&gt;# See what changed between releases&lt;/span&gt;
git diff v1.1.0 v1.2.0
git log v1.1.0..v1.2.0 &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When something breaks in production, &lt;code&gt;git diff v1.1.0 v1.2.0&lt;/code&gt; shows you exactly what changed between the version that worked and the version that didn't. Without tags, you're guessing which commits were in which release.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Collaboration Mindset
&lt;/h2&gt;

&lt;p&gt;Working on a shared repository is a form of communication. Every commit, PR, and branch name is a message to your team. Code that works but is unreadable is a burden. A PR that can't be reviewed is a bottleneck. A force push on &lt;code&gt;main&lt;/code&gt; is an interruption to everyone.&lt;/p&gt;

&lt;p&gt;The developers who collaborate well with Git aren't the ones with the most commands memorized. They're the ones who think about how their work will land for everyone else. Small PRs that are easy to review. Clear commit messages that explain decisions. Branches that are cleaned up after merging. History that tells the story of how the product was built.&lt;/p&gt;

&lt;p&gt;That history outlasts everyone on the team. Write it accordingly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update feature branch from main (clean)&lt;/span&gt;
git fetch origin
git rebase origin/main

&lt;span class="c"&gt;# Force push safely after rebase&lt;/span&gt;
git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt; origin feature/branch

&lt;span class="c"&gt;# Undo a commit on a shared branch (safe)&lt;/span&gt;
git revert HEAD
git push origin main

&lt;span class="c"&gt;# Create and push a release tag&lt;/span&gt;
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; v1.2.0 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Release 1.2.0"&lt;/span&gt;
git push origin v1.2.0

&lt;span class="c"&gt;# See what changed between releases&lt;/span&gt;
git diff v1.1.0 v1.2.0

&lt;span class="c"&gt;# See commits not yet in main&lt;/span&gt;
git log main..feature/branch &lt;span class="nt"&gt;--oneline&lt;/span&gt;

&lt;span class="c"&gt;# Check what you're about to push&lt;/span&gt;
git diff origin/main..HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;← &lt;a href="https://dev.to/itxshakil/branching-without-fear-65l"&gt;Part 3: Branching Without Fear&lt;/a&gt; | Next: &lt;a href="https://dev.to/itxshakil/git-as-your-safety-net-the-confidence-to-work-fearlessly-4k86"&gt;Part 5 — Git as Your Safety Net →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If this was useful, I turned the whole series into a 23-page PDF reference — checklists, hook templates, 80+ commands, reflog &amp;amp; bisect deep-dives, and a recovery playbook for 12 real emergencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://itxshakil.gumroad.com/l/git-mastery" rel="noopener noreferrer"&gt;Git Mastery Field Guide →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>git</category>
      <category>github</category>
    </item>
    <item>
      <title>Branching Without Fear</title>
      <dc:creator>Shakil Alam</dc:creator>
      <pubDate>Sun, 15 Mar 2026 08:27:02 +0000</pubDate>
      <link>https://dev.to/itxshakil/branching-without-fear-65l</link>
      <guid>https://dev.to/itxshakil/branching-without-fear-65l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 3 of the Git Mastery Series&lt;br&gt;
← &lt;a href="https://dev.to/itxshakil/committing-with-intention-the-art-of-a-good-commit-p90"&gt;Part 2: Committing with Intention&lt;/a&gt; | &lt;a href="https://dev.to/itxshakil/collaboration-that-doesnt-create-chaos-4k45"&gt;Part 4: Collaboration That Doesn't Create Chaos →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's a type of developer who avoids branches. They work directly on &lt;code&gt;main&lt;/code&gt;, commit everything there, and nervously push every fifteen minutes so their work is "safe." When asked why they don't branch, they say some version of: "It's just me" or "Branches feel like extra steps" or "I always mess up the merge."&lt;/p&gt;

&lt;p&gt;The irony is that branching is exactly what makes Git safe. It's the thing that lets you try something risky without touching working code. It's what lets you switch contexts instantly without losing your place. The developers who are most afraid of Git are often the ones least using the feature that would remove that fear.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Branch Is Letting You Do
&lt;/h2&gt;

&lt;p&gt;From Part 1, you know a branch is just a pointer. But let's talk about what that means practically.&lt;/p&gt;

&lt;p&gt;When you create a branch and switch to it, you're working in complete isolation. Whatever you commit doesn't touch &lt;code&gt;main&lt;/code&gt;. If you decide the whole experiment was wrong, you delete the branch and it's gone. &lt;code&gt;main&lt;/code&gt; never knew it existed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch &lt;span class="nt"&gt;-c&lt;/span&gt; experiment/try-new-payment-flow
&lt;span class="c"&gt;# ... write code, commit, test, realize it's a bad idea ...&lt;/span&gt;
git switch main
git branch &lt;span class="nt"&gt;-D&lt;/span&gt; experiment/try-new-payment-flow
&lt;span class="c"&gt;# Gone. main is unchanged.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the mental model: &lt;strong&gt;a branch is a sandbox&lt;/strong&gt;. Create one freely for every non-trivial piece of work — a feature, a fix, a refactor, even a quick experiment you're not sure about. The cost is near zero. The safety is real.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Branching Strategy That Actually Works
&lt;/h2&gt;

&lt;p&gt;There are entire books about branching strategies. GitFlow with its &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;release&lt;/code&gt;, &lt;code&gt;hotfix&lt;/code&gt;, and &lt;code&gt;feature&lt;/code&gt; branches. Trunk-based development. GitHub Flow. They all have tradeoffs.&lt;/p&gt;

&lt;p&gt;Here's the one that works for most teams most of the time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;main          → production-ready code, always deployable
feature/*     → new features, branched from main
bugfix/*      → bug fixes, branched from main  
hotfix/*      → urgent fixes, branched from main, merged back immediately
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No &lt;code&gt;develop&lt;/code&gt; branch creating a second place to merge things. No &lt;code&gt;release&lt;/code&gt; branches unless you genuinely need staged releases. Keep it flat until you have a specific reason not to.&lt;/p&gt;

&lt;p&gt;Naming matters more than developers think. &lt;code&gt;feature/user-login&lt;/code&gt; tells you everything. &lt;code&gt;feat-login-new&lt;/code&gt; tells you something. &lt;code&gt;johns-branch-v2&lt;/code&gt; tells you nothing and will confuse someone three months from now (including John).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Names that communicate intent&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/razorpay-integration
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; bugfix/cart-total-rounding-error
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; hotfix/payment-crash-on-null-address
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; refactor/extract-notification-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Merge vs Rebase: Stop Treating This as a Religion
&lt;/h2&gt;

&lt;p&gt;The merge-vs-rebase debate has taken on an almost theological quality in some developer communities. Strong opinions, passionate defense, occasional contempt for the other side.&lt;/p&gt;

&lt;p&gt;Here's the practical truth: they're different tools for different situations, and understanding what each one actually does makes the choice obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merge&lt;/strong&gt; preserves the full history of how things happened. When you merge &lt;code&gt;feature/login&lt;/code&gt; into &lt;code&gt;main&lt;/code&gt;, Git creates a merge commit that has two parents — the tip of &lt;code&gt;main&lt;/code&gt; and the tip of the feature branch. The history shows exactly when the branch was created and when it was merged. It's honest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch main
git merge feature/login
&lt;span class="c"&gt;# Creates a merge commit with two parents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rebase&lt;/strong&gt; replays your commits on top of another branch, creating new commits with the same changes but different parent hashes. The result looks like you wrote your feature on top of the latest &lt;code&gt;main&lt;/code&gt;, even if &lt;code&gt;main&lt;/code&gt; moved forward while you were working. The history is linear and clean. It's legible but slightly fictional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch feature/login
git rebase main
&lt;span class="c"&gt;# Replays your commits on top of current main&lt;/span&gt;
git switch main
git merge feature/login  &lt;span class="c"&gt;# Now a fast-forward, no merge commit needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use each:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use merge when you're bringing a finished feature into a shared branch. The merge commit is useful — it marks exactly when the feature landed.&lt;/p&gt;

&lt;p&gt;Use rebase when you're updating a feature branch to include recent changes from &lt;code&gt;main&lt;/code&gt;. This keeps your branch current without a messy "Merge branch 'main' into feature/login" commit polluting the feature's history.&lt;/p&gt;

&lt;p&gt;Use merge for public branches. Use rebase for your private, unpushed work. That's the rule that resolves 90% of the confusion.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fast-Forward and Why It Matters
&lt;/h2&gt;

&lt;p&gt;When a branch hasn't diverged from its target — meaning &lt;code&gt;main&lt;/code&gt; hasn't had any new commits since you branched off — Git can "fast-forward" instead of creating a merge commit. It just moves the pointer forward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# main: A → B → C&lt;/span&gt;
&lt;span class="c"&gt;# feature: A → B → C → D → E&lt;/span&gt;

git switch main
git merge feature
&lt;span class="c"&gt;# Result: main: A → B → C → D → E (no merge commit)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why rebasing before merging produces clean history — after a rebase, your branch is always ahead of main with no divergence, so the merge is always a fast-forward.&lt;/p&gt;

&lt;p&gt;If you want to &lt;em&gt;force&lt;/em&gt; a merge commit even when a fast-forward is possible (to preserve the record that a branch existed), use &lt;code&gt;--no-ff&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; feature/login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some teams do this for feature branches so every feature has a visible merge commit in history. Others prefer the clean linear history of fast-forwards. Both are defensible. The important thing is having a consistent team decision rather than random behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resolving Conflicts Without Panic
&lt;/h2&gt;

&lt;p&gt;Merge conflicts have a reputation they don't deserve. They're not dangerous. They're just Git telling you: "Two people edited the same area of the same file and I don't know which version to keep. You decide."&lt;/p&gt;

&lt;p&gt;That's it. Git is asking a question.&lt;/p&gt;

&lt;p&gt;When you hit a conflict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge feature/update-user-model
&lt;span class="c"&gt;# CONFLICT (content): Merge conflict in src/User.php&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;src/User.php&lt;/code&gt; and you'll see conflict markers:&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="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="na"&gt;HEAD&lt;/span&gt;
&lt;span class="na"&gt;protected&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="na"&gt;fillable = &lt;/span&gt;&lt;span class="s"&gt;['name',&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="err"&gt;',&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="err"&gt;'];&lt;/span&gt;
&lt;span class="err"&gt;=======&lt;/span&gt;
&lt;span class="na"&gt;protected&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="na"&gt;fillable = &lt;/span&gt;&lt;span class="s"&gt;['name',&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="err"&gt;',&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="err"&gt;'];&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; feature/update-user-model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The section between &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD&lt;/code&gt; and &lt;code&gt;=======&lt;/code&gt; is what your current branch has. The section between &lt;code&gt;=======&lt;/code&gt; and &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; is what you're merging in. You need to edit this into what it should actually be:&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;protected&lt;/span&gt; &lt;span class="nv"&gt;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'phone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add src/User.php
git commit  &lt;span class="c"&gt;# Complete the merge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things that make conflicts less painful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merge small, merge often.&lt;/strong&gt; A branch that diverges for two weeks will have far more conflicts than one that diverges for two days. The longer you wait, the more painful the merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a visual merge tool.&lt;/strong&gt; &lt;code&gt;git mergetool&lt;/code&gt; opens a three-pane editor showing the base, your changes, and theirs. It's significantly easier to understand than raw conflict markers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Talk to the other person first.&lt;/strong&gt; The best way to resolve a conflict is to understand &lt;em&gt;why&lt;/em&gt; both changes were made before deciding which to keep. A conflict is a conversation waiting to happen.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deleting Branches: Stop Hoarding
&lt;/h2&gt;

&lt;p&gt;Merged branches should be deleted. Not archived. Deleted.&lt;/p&gt;

&lt;p&gt;A repository with 200 stale branches is a repository nobody understands. You can't tell which branches are active, which are abandoned, which are merged. The signal is lost in noise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Delete local branch (after merging)&lt;/span&gt;
git branch &lt;span class="nt"&gt;-d&lt;/span&gt; feature/login

&lt;span class="c"&gt;# Delete remote branch&lt;/span&gt;
git push origin &lt;span class="nt"&gt;--delete&lt;/span&gt; feature/login

&lt;span class="c"&gt;# See remote branches that no longer exist locally&lt;/span&gt;
git remote prune origin &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most Git hosts (GitHub, GitLab, Bitbucket) offer "auto-delete branch on merge" in repository settings. Turn it on. Branches should be cheap to create and quick to discard — not collections you maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The One Branch Habit That Changes Everything
&lt;/h2&gt;

&lt;p&gt;Here's the habit that separates developers who love Git from developers who tolerate it: &lt;strong&gt;create a branch before you start anything, every time, even for small things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A "quick fix" that turns into a three-hour debugging session is exactly when you need a branch. A "tiny refactor" that somehow requires touching six files is exactly when you need a branch. An "experimental change" you might want to revert is exactly when you need a branch.&lt;/p&gt;

&lt;p&gt;The ten seconds to run &lt;code&gt;git switch -c fix/whatever&lt;/code&gt; is worth it every single time. It creates a clean separation between "finished, working code" and "work in progress." It means &lt;code&gt;main&lt;/code&gt; is always in a deployable state. It means you can abandon your work cleanly if something higher priority comes up.&lt;/p&gt;

&lt;p&gt;Once this habit becomes automatic, Git starts feeling like a safety net rather than a source of anxiety. Because that's what it is.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create and switch to a new branch&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/branch-name

&lt;span class="c"&gt;# Switch to existing branch&lt;/span&gt;
git switch branch-name

&lt;span class="c"&gt;# List all branches (including remote)&lt;/span&gt;
git branch &lt;span class="nt"&gt;-a&lt;/span&gt;

&lt;span class="c"&gt;# Merge a branch into current&lt;/span&gt;
git merge branch-name

&lt;span class="c"&gt;# Rebase current branch onto another&lt;/span&gt;
git rebase main

&lt;span class="c"&gt;# Delete merged local branch&lt;/span&gt;
git branch &lt;span class="nt"&gt;-d&lt;/span&gt; branch-name

&lt;span class="c"&gt;# Force delete unmerged branch&lt;/span&gt;
git branch &lt;span class="nt"&gt;-D&lt;/span&gt; branch-name

&lt;span class="c"&gt;# Delete remote branch&lt;/span&gt;
git push origin &lt;span class="nt"&gt;--delete&lt;/span&gt; branch-name

&lt;span class="c"&gt;# See branches with last commit&lt;/span&gt;
git branch &lt;span class="nt"&gt;-v&lt;/span&gt;

&lt;span class="c"&gt;# See which branches are merged into main&lt;/span&gt;
git branch &lt;span class="nt"&gt;--merged&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;← &lt;a href="https://dev.to/itxshakil/committing-with-intention-the-art-of-a-good-commit-p90"&gt;Part 2: Committing with Intention&lt;/a&gt; | Next: &lt;a href="https://dev.to/itxshakil/collaboration-that-doesnt-create-chaos-4k45"&gt;Part 4 — Collaboration That Doesn't Create Chaos →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If this was useful, I turned the whole series into a 23-page PDF reference — checklists, hook templates, 80+ commands, reflog &amp;amp; bisect deep-dives, and a recovery playbook for 12 real emergencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://itxshakil.gumroad.com/l/git-mastery" rel="noopener noreferrer"&gt;Git Mastery Field Guide →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>git</category>
      <category>github</category>
    </item>
  </channel>
</rss>
