<?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: Sumeet Shroff</title>
    <description>The latest articles on DEV Community by Sumeet Shroff (@mumbai_web_designer).</description>
    <link>https://dev.to/mumbai_web_designer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3983179%2F2edf10ed-7d57-4ac5-987f-d8afa268d641.png</url>
      <title>DEV Community: Sumeet Shroff</title>
      <link>https://dev.to/mumbai_web_designer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mumbai_web_designer"/>
    <language>en</language>
    <item>
      <title>Laravel Cloud vs Forge vs a Self-Hosted VPS</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Thu, 03 Sep 2026 10:01:05 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/laravel-cloud-vs-forge-vs-a-self-hosted-vps-2976</link>
      <guid>https://dev.to/mumbai_web_designer/laravel-cloud-vs-forge-vs-a-self-hosted-vps-2976</guid>
      <description>&lt;h1&gt;
  
  
  Laravel Cloud vs Forge vs a Self-Hosted VPS
&lt;/h1&gt;

&lt;p&gt;Three paths exist for getting a Laravel application into production: let Laravel Cloud handle everything, use Forge to manage your own servers, or configure a VPS from scratch. Each represents a different point on the control-vs-convenience spectrum, and picking the wrong one costs you either money, engineering time, or both.&lt;/p&gt;

&lt;p&gt;This article cuts straight to the trade-offs. It assumes you already understand basic Laravel deployment concepts — if you want the broader picture, the &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-deployment-hosting-guide" rel="noopener noreferrer"&gt;Laravel Deployment and Hosting: Cloud, Forge, VPS, Docker, and Octane&lt;/a&gt; guide covers the full stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Laravel 12 (PHP 8.2+) or Laravel 13 (PHP 8.3+), Composer 2.x, a GitHub/GitLab repository. For Forge: an account at forge.laravel.com. For Laravel Cloud: an account at cloud.laravel.com.&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Laravel Cloud&lt;/th&gt;
&lt;th&gt;Forge + VPS&lt;/th&gt;
&lt;th&gt;Self-Hosted VPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure owner&lt;/td&gt;
&lt;td&gt;Laravel/AWS EC2&lt;/td&gt;
&lt;td&gt;You (via DigitalOcean, Hetzner, etc.)&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH access&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (full root)&lt;/td&gt;
&lt;td&gt;Yes (full root)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment trigger&lt;/td&gt;
&lt;td&gt;Git push&lt;/td&gt;
&lt;td&gt;Git push or Forge webhook&lt;/td&gt;
&lt;td&gt;Custom CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-scaling&lt;/td&gt;
&lt;td&gt;Yes (per-replica)&lt;/td&gt;
&lt;td&gt;No (manual resize)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;Usage-based&lt;/td&gt;
&lt;td&gt;Flat monthly ($12–$29/mo for Forge + server cost)&lt;/td&gt;
&lt;td&gt;Server cost only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-downtime deploys&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Built-in (single server)&lt;/td&gt;
&lt;td&gt;Manual setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops overhead&lt;/td&gt;
&lt;td&gt;Near zero&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Laravel Cloud
&lt;/h2&gt;

&lt;p&gt;Launched on February 24, 2025 alongside Laravel 12, Laravel Cloud is a fully managed, serverful platform running on Amazon EC2. "Serverful" is the important word — your application container stays warm between requests. There are no Lambda cold starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does automatically:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provisions isolated environments (production, staging, preview per branch)&lt;/li&gt;
&lt;li&gt;Runs &lt;code&gt;composer install --no-dev&lt;/code&gt;, config/route/view cache, and migrations on each deploy&lt;/li&gt;
&lt;li&gt;Scales replicas up and down based on traffic&lt;/li&gt;
&lt;li&gt;Rotates SSL certificates&lt;/li&gt;
&lt;li&gt;Manages encrypted environment variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you cannot do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH into the underlying instance&lt;/li&gt;
&lt;li&gt;Install custom PHP extensions not included in the platform image&lt;/li&gt;
&lt;li&gt;Configure Nginx directly&lt;/li&gt;
&lt;li&gt;Run long-lived daemons outside of the provided worker abstraction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; The Starter plan carries no monthly subscription fee — you pay only for compute and bandwidth consumed. Growth plans begin at $20/month with higher resource ceilings. Pricing is usage-based, which is predictable when traffic is steady but can spike with traffic bursts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to choose Cloud:&lt;/strong&gt;&lt;br&gt;
You want zero ops overhead. Your team does not have a dedicated DevOps engineer. Traffic is variable and you want auto-scaling without provisioning extra capacity manually. You are comfortable with the Laravel ecosystem's managed environment and the associated vendor dependency.&lt;/p&gt;


&lt;h2&gt;
  
  
  Laravel Forge
&lt;/h2&gt;

&lt;p&gt;Forge is a server management panel, not a hosting provider. You bring the cloud server — DigitalOcean, AWS, Hetzner, Vultr, or any provider that lets Forge connect via API. Forge then provisions Ubuntu, configures Nginx, PHP-FPM, MySQL or PostgreSQL, Redis, and Supervisor, and wires up your deployment pipeline.&lt;/p&gt;

&lt;p&gt;The October 2025 overhaul introduced &lt;strong&gt;Laravel VPS&lt;/strong&gt; — a DigitalOcean-backed product within Forge that provisions an Ubuntu server in seconds with consolidated billing. You get full SSH and root access, but Forge handles the initial stack configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero-downtime deployments (enabled by default for new Forge sites):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forge uses an atomic symlink strategy. Each deployment clones your repository into a timestamped directory under &lt;code&gt;releases/&lt;/code&gt;, runs your deployment script, then switches the &lt;code&gt;current&lt;/code&gt; symlink. The last four releases are retained for instant rollback.&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;# Forge deployment script (configured in Forge dashboard)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /home/forge/example.com
git pull origin main

composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--optimize-autoloader&lt;/span&gt; &lt;span class="nt"&gt;--no-interaction&lt;/span&gt;

php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan migrate &lt;span class="nt"&gt;--force&lt;/span&gt;

php artisan queue:restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Critical note: &lt;code&gt;php artisan migrate --force&lt;/code&gt; bypasses the production confirmation prompt. Always test migrations on a staging server first — schema changes can cause irreversible data loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared paths configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For zero-downtime deployments, &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;storage/&lt;/code&gt; must persist across releases. In Forge's dashboard, under a site's zero-downtime settings, add these shared paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;.env&lt;/span&gt;
&lt;span class="err"&gt;storage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forgetting this causes each release to deploy with an empty storage directory — uploaded files vanish and sessions break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-server limitation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forge's atomic deployments work on a single server only. If you run two application servers behind a load balancer, Forge cannot coordinate a simultaneous atomic deploy across both. For that you need Laravel Envoyer, which deploys a single project across multiple servers with health checks before traffic is switched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to choose Forge:&lt;/strong&gt;&lt;br&gt;
You want full server control — custom Nginx configs, specific PHP extensions, multiple queue worker groups, PostgreSQL 18 (now supported in Forge for new deployments), or the ability to SSH in when things go wrong. Traffic is relatively predictable and a fixed-size server covers your load. You want flat-rate pricing.&lt;/p&gt;


&lt;h2&gt;
  
  
  Self-Hosted VPS (No Forge)
&lt;/h2&gt;

&lt;p&gt;Provisioning and configuring a VPS without a panel means owning every layer: OS updates, Nginx config, PHP-FPM pools, SSL renewal, firewall rules, and deployment scripting. This is operationally expensive but gives you complete control and the lowest possible cost per compute unit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Baseline production stack (Ubuntu 24.04 LTS):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/nginx/sites-available/example.com&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/current/public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.php?&lt;/span&gt;&lt;span class="nv"&gt;$query_string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;unix:/var/run/php/php8.3-fpm.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$realpath_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&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;Queue worker via Supervisor:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/supervisor/conf.d/laravel-worker.conf
&lt;/span&gt;&lt;span class="nn"&gt;[program:laravel-worker]&lt;/span&gt;
&lt;span class="py"&gt;process_name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%(program_name)s_%(process_num)02d&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;php /var/www/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600&lt;/span&gt;
&lt;span class="py"&gt;autostart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;stopasgroup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;killasgroup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;www-data&lt;/span&gt;
&lt;span class="py"&gt;numprocs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;
&lt;span class="py"&gt;redirect_stderr&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;stdout_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/www/current/storage/logs/worker.log&lt;/span&gt;
&lt;span class="py"&gt;stopwaitsecs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;sudo supervisorctl reread &amp;amp;&amp;amp; sudo supervisorctl update &amp;amp;&amp;amp; sudo supervisorctl start laravel-worker:*&lt;/code&gt; after adding the config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After every deployment, restart long-running processes:&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;php artisan queue:restart
&lt;span class="nb"&gt;sudo &lt;/span&gt;supervisorctl restart laravel-worker:&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New application code is NOT picked up by queue workers until they restart. This is the most common production bug in self-managed deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to choose a bare VPS:&lt;/strong&gt;&lt;br&gt;
You are running predictable, steady-state traffic on a tight budget. A $6–12/month DigitalOcean or Hetzner droplet handles substantial CRUD traffic. You have the ops experience to manage patching, backups, and incident response, or you are willing to learn.&lt;/p&gt;


&lt;h2&gt;
  
  
  Common Mistakes Across All Three
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Calling &lt;code&gt;env()&lt;/code&gt; outside config files after config:cache&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once &lt;code&gt;php artisan config:cache&lt;/code&gt; runs, &lt;code&gt;.env&lt;/code&gt; is no longer read at runtime. Any &lt;code&gt;env()&lt;/code&gt; call that is not inside a &lt;code&gt;config/*.php&lt;/code&gt; file returns &lt;code&gt;null&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;// WRONG — returns null in production after config:cache&lt;/span&gt;
&lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'STRIPE_SECRET'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// CORRECT — read from config, which was cached from .env&lt;/span&gt;
&lt;span class="nv"&gt;$apiKey&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;'services.stripe.secret'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Not restarting Octane or queue workers after deploy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel Cloud handles this automatically. Forge's deployment script must include &lt;code&gt;php artisan queue:restart&lt;/code&gt;. A bare VPS requires a Supervisor restart command in your CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Exposing database ports publicly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MySQL, PostgreSQL, and Redis should never be accessible on public interfaces. On a bare VPS, configure UFW to block ports 3306, 5432, and 6379 from external access. Forge provides a firewall UI to do this from the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. APP_DEBUG=true in production&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;APP_DEBUG=true&lt;/code&gt; and CVE-2024-13918/CVE-2024-13919 present, Laravel's debug error page reflects request parameters unescaped, enabling reflected XSS. Always set &lt;code&gt;APP_DEBUG=false&lt;/code&gt; in production &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Skipping &lt;code&gt;--no-dev&lt;/code&gt; on composer install&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Development dependencies (PHPUnit, Mockery, Laravel Pint) add 30–50 MB to the vendor directory and introduce unnecessary attack surface. Always use &lt;code&gt;composer install --no-dev --optimize-autoloader&lt;/code&gt; in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Checklist Before Going Live
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Patch to Laravel 12.1.1+ or 11.44.1+ to resolve CVE-2025-27515 (file validation bypass)&lt;/li&gt;
&lt;li&gt;If using Livewire, update to v3.6.4+ — v3.6.3 and below carry CVE-2025-54068, a critical RCE vulnerability&lt;/li&gt;
&lt;li&gt;Confirm &lt;code&gt;register_argc_argv&lt;/code&gt; is disabled in &lt;code&gt;php.ini&lt;/code&gt; (mitigates CVE-2024-52301, CVSS 8.7)&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;APP_KEY&lt;/code&gt; is unique per environment — sharing keys between staging and production allows session/cookie forgery&lt;/li&gt;
&lt;li&gt;Never bake &lt;code&gt;.env&lt;/code&gt; files or secrets into Docker images&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Verification After Deployment
&lt;/h2&gt;

&lt;p&gt;Laravel 12 introduced native health check routes requiring no extra packages. Hit the endpoint after each deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/up
&lt;span class="c"&gt;# Returns HTTP 200 with JSON: {"status":"ok"} when healthy&lt;/span&gt;
&lt;span class="c"&gt;# Reports database, cache, and queue status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Forge, you can configure this URL as a deployment health check — Forge aborts the symlink switch if the health endpoint returns a non-200 status, preventing a broken release from going live.&lt;/p&gt;

&lt;p&gt;For bare VPS deployments, add the health check as the final step in your CI/CD pipeline:&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;# In your deploy script — abort if health check fails&lt;/span&gt;
&lt;span class="nv"&gt;HEALTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://example.com/up&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HEALTH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &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;"Health check failed (&lt;/span&gt;&lt;span class="nv"&gt;$HEALTH&lt;/span&gt;&lt;span class="s2"&gt;) — rolling back"&lt;/span&gt;
  &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sfn&lt;/span&gt; /var/www/releases/previous /var/www/current
  &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;h2&gt;
  
  
  Decision Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel Cloud&lt;/strong&gt; if you want push-to-deploy with zero server management and are comfortable with usage-based pricing and no SSH access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forge&lt;/strong&gt; if you want managed deployment tooling, full SSH access, predictable flat-rate billing, and the ability to customise your stack (PHP extensions, PostgreSQL 18, custom Nginx blocks).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bare VPS&lt;/strong&gt; if you have the ops expertise to manage the full stack, want the lowest cost per compute unit, and need maximum control over every layer of the environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the large majority of Laravel projects — especially agencies and product teams without dedicated DevOps — Forge with a mid-tier DigitalOcean or Hetzner droplet is the pragmatic default. Laravel Cloud is compelling when auto-scaling is a hard requirement. A bare VPS pays off only when the ops cost is genuinely lower than the time saved by a managed tool.&lt;/p&gt;




&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>devops</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Dev.to — Alpine.js Patterns for Dynamic Laravel Forms</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Thu, 03 Sep 2026 08:01:51 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/devto-alpinejs-patterns-for-dynamic-laravel-forms-1ebd</link>
      <guid>https://dev.to/mumbai_web_designer/devto-alpinejs-patterns-for-dynamic-laravel-forms-1ebd</guid>
      <description>&lt;h1&gt;
  
  
  Alpine.js Patterns for Dynamic Laravel Forms
&lt;/h1&gt;

&lt;p&gt;If you're already using Laravel with Blade templates and want form interactions without reaching for a full JavaScript framework, Alpine.js is the right tool. At roughly 15 KB gzipped, it handles conditional fields, multi-step flows, dynamic repeater rows, and inline validation feedback — all declared directly in your HTML attributes.&lt;/p&gt;

&lt;p&gt;This article focuses specifically on form patterns: the practical, real-world cases where Alpine.js saves you from writing dozens of lines of jQuery or wiring up a full Vue component for something that's really just three state variables. If you want the broader picture of where Alpine fits in the Laravel frontend ecosystem alongside Livewire 4, Inertia, React, and Vue, see &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-frontends-livewire-inertia" rel="noopener noreferrer"&gt;Modern Laravel Frontends: Livewire 4, Inertia, React, Vue, and Alpine.js&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and Versions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Laravel 11 or 12&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpine.js v3.15.x&lt;/strong&gt; (latest stable as of June 2026 is v3.15.11, released April 1, 2026)&lt;/li&gt;
&lt;li&gt;Blade templates (no Livewire required for any pattern in this article)&lt;/li&gt;
&lt;li&gt;Basic familiarity with &lt;code&gt;x-data&lt;/code&gt;, &lt;code&gt;x-show&lt;/code&gt;, and &lt;code&gt;x-model&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install Alpine via CDN (fast prototyping) or npm (production):&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;!-- CDN --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;defer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/alpinejs@3.15.11/dist/cdn.min.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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# npm&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;alpinejs@^3.15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// resources/js/app.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Alpine&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alpinejs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;Alpine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Alpine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;Alpine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Pattern 1 — Conditional Fields
&lt;/h2&gt;

&lt;p&gt;The most common Alpine form pattern: show or hide fields based on the value of another field. A service enquiry form that reveals extra fields when "Custom Package" is selected:&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;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/enquiry"&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"{ serviceType: '' }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    @csrf

    &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"service_type"&lt;/span&gt; &lt;span class="na"&gt;x-model=&lt;/span&gt;&lt;span class="s"&gt;"serviceType"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Select a service&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"web-design"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Website Design&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"seo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;SEO&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"custom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Custom Package&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"serviceType === 'custom'"&lt;/span&gt; &lt;span class="na"&gt;x-transition&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Describe your requirements&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"custom_requirements"&lt;/span&gt; &lt;span class="na"&gt;rows=&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Estimated budget (INR)&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"budget"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"e.g. 50000"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send Enquiry&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;style="display:none"&lt;/code&gt; on the conditional &lt;code&gt;div&lt;/code&gt; ensures it's hidden on page load before Alpine initialises — preventing a flash of unwanted content. &lt;code&gt;x-transition&lt;/code&gt; adds a smooth opacity-and-scale entrance.&lt;/p&gt;

&lt;p&gt;On the Laravel side, your validation rule conditionally requires the textarea only when &lt;code&gt;service_type&lt;/code&gt; is &lt;code&gt;custom&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;// app/Http/Controllers/EnquiryController.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;RedirectResponse&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;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'service_type'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'custom_requirements'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required_if:service_type,custom'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:1000'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'budget'&lt;/span&gt;              &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required_if:service_type,custom'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'min:1000'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="c1"&gt;// store enquiry...&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;back&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Enquiry submitted.'&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;Keep validation on the server. Alpine's conditional display is a UX convenience, not a security gate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 2 — Dynamic Repeater Rows
&lt;/h2&gt;

&lt;p&gt;Allowing users to add multiple entries — team members, line items, phone numbers — without page reloads. This pattern uses Alpine's &lt;code&gt;x-for&lt;/code&gt; with a reactive array.&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;div&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"{
    contacts: [{ name: '', phone: '' }],
    addRow()  { this.contacts.push({ name: '', phone: '' }); },
    removeRow(index) { this.contacts.splice(index, 1); }
}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;x-for=&lt;/span&gt;&lt;span class="s"&gt;"(contact, index) in contacts"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"index"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-3 mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
                &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
                &lt;span class="na"&gt;:name=&lt;/span&gt;&lt;span class="s"&gt;"'contacts[' + index + '][name]'"&lt;/span&gt;
                &lt;span class="na"&gt;x-model=&lt;/span&gt;&lt;span class="s"&gt;"contact.name"&lt;/span&gt;
                &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Full name"&lt;/span&gt;
            &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
                &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"tel"&lt;/span&gt;
                &lt;span class="na"&gt;:name=&lt;/span&gt;&lt;span class="s"&gt;"'contacts[' + index + '][phone]'"&lt;/span&gt;
                &lt;span class="na"&gt;x-model=&lt;/span&gt;&lt;span class="s"&gt;"contact.phone"&lt;/span&gt;
                &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Phone number"&lt;/span&gt;
            &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"removeRow(index)"&lt;/span&gt;
                    &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"contacts.length &amp;gt; 1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                Remove
            &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"addRow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add Contact&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;:name&lt;/code&gt; binding uses the array index to produce &lt;code&gt;contacts[0][name]&lt;/code&gt;, &lt;code&gt;contacts[1][name]&lt;/code&gt;, etc. Laravel's &lt;code&gt;request()-&amp;gt;input('contacts')&lt;/code&gt; returns exactly this as a nested array.&lt;/p&gt;

&lt;p&gt;Validate nested input in your controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'contacts'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'min:1'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'contacts.*.name'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:100'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'contacts.*.phone'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'regex:/^[6-9]\d{9}$/'&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;Note on &lt;code&gt;x-for&lt;/code&gt; and Alpine v3.15: Alpine now supports &lt;code&gt;Set&lt;/code&gt; objects inside &lt;code&gt;x-for&lt;/code&gt;, and template-based &lt;code&gt;x-sort&lt;/code&gt; handles work correctly — but for basic repeater patterns, arrays remain the simplest choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 3 — Multi-Step Form with Progress
&lt;/h2&gt;

&lt;p&gt;Breaking a long form into steps keeps users on task. Alpine manages the current step index; all form fields live inside a single &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; so a single POST submits everything.&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;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/project-brief"&lt;/span&gt;
      &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"{ step: 1, totalSteps: 3 }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    @csrf

    {{-- Step indicator --}}
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Step &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"step"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt; of &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"totalSteps"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-2 bg-gray-200 rounded"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-2 bg-blue-600 rounded"&lt;/span&gt;
             &lt;span class="na"&gt;:style=&lt;/span&gt;&lt;span class="s"&gt;"'width:' + (step / totalSteps * 100) + '%'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    {{-- Step 1: Contact details --}}
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"step === 1"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;  &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Your name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email address"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    {{-- Step 2: Project type --}}
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"step === 2"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"project_type"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"ecommerce"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Ecommerce Website&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"corporate"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Corporate Website&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"landing"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Landing Page&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    {{-- Step 3: Budget and deadline --}}
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"step === 3"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"budget"&lt;/span&gt;   &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Budget in INR"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt;   &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"deadline"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    {{-- Navigation --}}
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"step--"&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"step &amp;gt; 1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Back&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"step++"&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"step &amp;lt; totalSteps"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Next&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;                 &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"step === totalSteps"&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;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach has a deliberate limitation: client-side step validation is not enforced. A user can click submit on step 3 with step 1 fields empty — Laravel's server-side validation catches this and returns errors. If you need per-step client validation, add a &lt;code&gt;validateStep()&lt;/code&gt; method to your &lt;code&gt;x-data&lt;/code&gt; that checks required fields before advancing. Keep that lightweight — complex validation logic is a signal to consider Livewire instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 4 — Character Counter and Inline Feedback
&lt;/h2&gt;

&lt;p&gt;Real-time character counts and field-level feedback with zero server round-trips:&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;div&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"{
    message: '',
    maxChars: 500,
    get remaining() { return this.maxChars - this.message.length; },
    get isOverLimit() { return this.message.length &amp;gt; this.maxChars; }
}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt;
        &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;
        &lt;span class="na"&gt;x-model=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;
        &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"{ 'border-red-500': isOverLimit }"&lt;/span&gt;
        &lt;span class="na"&gt;rows=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt;
        &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Tell us about your project..."&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"isOverLimit ? 'text-red-600' : 'text-gray-500'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"remaining"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt; characters remaining
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getter properties (&lt;code&gt;get remaining()&lt;/code&gt;, &lt;code&gt;get isOverLimit()&lt;/code&gt;) in Alpine &lt;code&gt;x-data&lt;/code&gt; objects work cleanly and re-evaluate reactively whenever &lt;code&gt;message&lt;/code&gt; changes. This avoids storing derived state as separate reactive properties.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 5 — AJAX Submission Without Page Reload
&lt;/h2&gt;

&lt;p&gt;Alpine can post a form via &lt;code&gt;fetch&lt;/code&gt; and display success or error messages inline. This is appropriate for single-action widgets like a newsletter signup or a quick quote request — not for complex multi-field forms where error mapping across many fields gets unwieldy.&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;div&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"{
    email: '',
    status: null,
    errorMsg: '',
    async submit() {
        this.status = 'loading';
        const res = await fetch('/newsletter/subscribe', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
                'Accept': 'application/json',
            },
            body: JSON.stringify({ email: this.email }),
        });
        if (res.ok) {
            this.status = 'success';
        } else {
            const data = await res.json();
            this.errorMsg = data.message ?? 'Something went wrong.';
            this.status = 'error';
        }
    }
}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;x-model=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Enter your email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"status === 'loading'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"status !== 'loading'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Subscribe&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"status === 'loading'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Subscribing...&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"status === 'success'"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-green-600"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;You're subscribed!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;x-show=&lt;/span&gt;&lt;span class="s"&gt;"status === 'error'"&lt;/span&gt;   &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-red-600"&lt;/span&gt;   &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"errorMsg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CSRF token is read from the meta tag — always include &lt;code&gt;&amp;lt;meta name="csrf-token" content="{{ csrf_token() }}"&amp;gt;&lt;/code&gt; in your layout. The Laravel controller returns &lt;code&gt;response()-&amp;gt;json()&lt;/code&gt; with appropriate status codes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;style="display:none"&lt;/code&gt; on &lt;code&gt;x-show&lt;/code&gt; elements.&lt;/strong&gt; Without it, elements flash visible before Alpine boots, especially on slower devices. Always add the inline style to any element using &lt;code&gt;x-show&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Putting business logic in Alpine expressions.&lt;/strong&gt; Alpine evaluates expressions as JavaScript. Injecting any user-controlled string into an Alpine expression (&lt;code&gt;x-bind&lt;/code&gt;, &lt;code&gt;x-on&lt;/code&gt;, &lt;code&gt;x-data&lt;/code&gt;) creates an XSS vector. Never dynamically construct Alpine directives from server-rendered user data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skipping server-side validation because Alpine handles it client-side.&lt;/strong&gt; Alpine validation is strictly a UX enhancement. Every form submission must be validated in Laravel. Period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using Alpine for forms with 10+ interdependent fields.&lt;/strong&gt; When your &lt;code&gt;x-data&lt;/code&gt; object grows beyond 6–8 properties with multiple watchers and computed properties, maintainability drops. That's the signal to move to a Livewire component where PHP handles the logic and you get a clean class structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not accounting for Alpine's CSP constraint.&lt;/strong&gt; Alpine evaluates expressions using &lt;code&gt;new Function()&lt;/code&gt;, which requires &lt;code&gt;unsafe-eval&lt;/code&gt; in your Content Security Policy. If you need a strict CSP, use the Alpine CSP build (&lt;code&gt;alpinejs/dist/cdn-csp.min.js&lt;/code&gt;). Be aware that enabling Livewire 4's &lt;code&gt;csp_safe&lt;/code&gt; mode in &lt;code&gt;config/livewire.php&lt;/code&gt; also forces the entire app to use Alpine's CSP evaluator, which restricts complex expressions in directives app-wide.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing Alpine Forms
&lt;/h2&gt;

&lt;p&gt;Alpine state is entirely client-side, so testing happens at two levels:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel feature tests&lt;/strong&gt; — submit the form via HTTP and assert validation behaviour:&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;// tests/Feature/EnquiryFormTest.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_custom_package_requires_requirements&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;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/enquiry'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'service_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'custom'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// missing custom_requirements&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;assertSessionHasErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'custom_requirements'&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;Browser tests&lt;/strong&gt; — use Laravel Dusk to verify Alpine behaviour in a real browser:&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;// tests/Browser/EnquiryFormTest.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_custom_fields_appear_on_selection&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;browse&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;Browser&lt;/span&gt; &lt;span class="nv"&gt;$browser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$browser&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/enquiry'&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;assertMissing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'textarea[name=custom_requirements]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'service_type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'custom'&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;waitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'textarea[name=custom_requirements]'&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;assertVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'textarea[name=custom_requirements]'&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;Dusk runs a real Chrome instance, so &lt;code&gt;x-show&lt;/code&gt;, &lt;code&gt;x-transition&lt;/code&gt;, and &lt;code&gt;x-model&lt;/code&gt; behaviour is tested accurately.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Alpine Is Enough vs When It Isn't
&lt;/h2&gt;

&lt;p&gt;Alpine is the right choice when your form interaction is primarily about &lt;strong&gt;showing/hiding, counting, or posting to a single endpoint&lt;/strong&gt;. It is the wrong choice when you need server-side reactivity (e.g., fetching a dynamic price from the database as the user changes fields, or running a real-time search). Those scenarios belong in Livewire, where every user interaction triggers a PHP method and Laravel handles the logic.&lt;/p&gt;

&lt;p&gt;The practical line: if solving the problem requires a &lt;code&gt;fetch&lt;/code&gt; call that returns structured data (not just a success/error), Alpine starts to fight against you and a Livewire component is cleaner.&lt;/p&gt;




&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Dev.to — Laravel with React or Vue through Inertia</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Thu, 03 Sep 2026 08:00:46 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/devto-laravel-with-react-or-vue-through-inertia-nj0</link>
      <guid>https://dev.to/mumbai_web_designer/devto-laravel-with-react-or-vue-through-inertia-nj0</guid>
      <description>&lt;h1&gt;
  
  
  Laravel with React or Vue through Inertia
&lt;/h1&gt;

&lt;p&gt;If you've spent time with Laravel and wondered how to use React or Vue without abandoning the monolith, Inertia.js is the answer. It bridges the gap between a server-driven Laravel backend and a client-rendered component frontend — no separate API, no JSON contract negotiation, no duplicated routing layer.&lt;/p&gt;

&lt;p&gt;This article focuses on the practical setup: how Inertia actually works, how to scaffold it with React or Vue, and where it makes sense compared to going full-SPA or staying with Blade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites and Versions
&lt;/h2&gt;

&lt;p&gt;Before starting, confirm your stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 11 or 12&lt;/strong&gt; — required for the Inertia Laravel adapter v3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inertia.js v3.0.0&lt;/strong&gt; (stable since March 25, 2026) — current default&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inertia.js v2.x&lt;/strong&gt; — still supported; official Laravel starter kits ship with v2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React 19&lt;/strong&gt; or &lt;strong&gt;Vue 3&lt;/strong&gt; — both work with Inertia v2 and v3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 20+&lt;/strong&gt; and &lt;strong&gt;Vite&lt;/strong&gt; for the frontend build pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composer&lt;/strong&gt; and &lt;strong&gt;pnpm&lt;/strong&gt; (or npm)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are on Laravel 10, you are capped at Inertia adapter v2. The v3 adapter requires Laravel 11 minimum.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Inertia.js Works (The Mental Model)
&lt;/h2&gt;

&lt;p&gt;Inertia is not a UI framework. It is a protocol adapter that sits between Laravel controllers and React/Vue components.&lt;/p&gt;

&lt;p&gt;On a full-page load, Laravel renders an HTML shell with a single &lt;code&gt;&amp;lt;div id="app"&amp;gt;&lt;/code&gt; that contains a JSON payload in a &lt;code&gt;data-page&lt;/code&gt; attribute. Inertia's client-side adapter bootstraps React or Vue, reads that payload, and renders the correct page component.&lt;/p&gt;

&lt;p&gt;On subsequent navigations, clicking an &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; component triggers a fetch request with an &lt;code&gt;X-Inertia: true&lt;/code&gt; header. Laravel detects this header and returns only JSON — the component name and its props — instead of a full HTML page. The client swaps the component without a full browser reload.&lt;/p&gt;

&lt;p&gt;The result: your Laravel routes, controllers, and auth middleware work exactly as they always did. Your frontend developers get full React or Vue components with props typed exactly as the controller passes them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scaffolding a New Project
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A — Official Laravel Starter Kit (Fastest)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# React 19 + Inertia 2 + Tailwind 4 + shadcn/ui&lt;/span&gt;
laravel new my-app &lt;span class="nt"&gt;--react&lt;/span&gt;

&lt;span class="c"&gt;# Vue 3 + Inertia 2 + Tailwind 4&lt;/span&gt;
laravel new my-app &lt;span class="nt"&gt;--vue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: as of mid-2026, the official starter kits ship with Inertia &lt;strong&gt;v2&lt;/strong&gt;, not v3. Inertia v3 must be manually upgraded after scaffolding if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B — Breeze with Inertia Stack
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/breeze &lt;span class="nt"&gt;--dev&lt;/span&gt;
php artisan breeze:install react
&lt;span class="c"&gt;# or&lt;/span&gt;
php artisan breeze:install vue

npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breeze installs authentication scaffolding (login, register, password reset) wired through Inertia pages. It's a solid starting point for applications that need auth out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C — Manual Inertia Install
&lt;/h3&gt;

&lt;p&gt;For projects where you need fine-grained control:&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;# Server side&lt;/span&gt;
composer require inertiajs/inertia-laravel

&lt;span class="c"&gt;# Publish middleware&lt;/span&gt;
php artisan inertia:middleware
&lt;span class="c"&gt;# Register HandleInertiaRequests in bootstrap/app.php&lt;/span&gt;

&lt;span class="c"&gt;# Client side — React&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @inertiajs/react react react-dom

&lt;span class="c"&gt;# Or Vue&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @inertiajs/vue3 vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Root template&lt;/strong&gt; (&lt;code&gt;resources/views/app.blade.php&lt;/code&gt;):&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="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    @viteReactRefresh {{-- React only --}}
    @vite(['resources/js/app.jsx', 'resources/css/app.css'])
    @inertiaHead
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    @inertia
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;React entrypoint&lt;/strong&gt; (&lt;code&gt;resources/js/app.jsx&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createInertiaApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inertiajs/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRoot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;createInertiaApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Pages/**/*.jsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;eager&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`./Pages/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.jsx`&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;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;Vue entrypoint&lt;/strong&gt; (&lt;code&gt;resources/js/app.js&lt;/code&gt;):&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createInertiaApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inertiajs/vue3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;createInertiaApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Pages/**/*.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;eager&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`./Pages/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.vue`&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;render&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;h&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&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="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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;
  
  
  Writing Controllers and Page Components
&lt;/h2&gt;

&lt;p&gt;A Laravel controller renders an Inertia page the same way it would render a Blade view — with one method call:&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;Inertia\Inertia&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;ProductController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&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;Inertia&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Products/Index'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;latest&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;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="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;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Inertia&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Products/Show'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'product'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reviews'&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 matching React page component at &lt;code&gt;resources/js/Pages/Products/Index.jsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;products&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Products&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Props flow one way: controller → component. There is no need to define API endpoints, serializers, or fetch logic for standard page loads.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inertia v2 Features Worth Using
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Deferred Props
&lt;/h3&gt;

&lt;p&gt;For data that is expensive to compute and not needed for the initial render:&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;// Controller&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Inertia&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'user'&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="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'stats'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Inertia&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStats&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="s1"&gt;'activity'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Inertia&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getActivity&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'secondary'&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React component&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Deferred&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inertiajs/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Deferred&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"stats"&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading stats...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StatsChart&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Deferred&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stats&lt;/code&gt; loads in a second request after the page renders. Props in the same group string (&lt;code&gt;'secondary'&lt;/code&gt;) are fetched in a single batched request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefetching
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Link&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inertiajs/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Prefetch after 75ms hover&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt; &lt;span class="na"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Users&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Prefetch on mount (useful for near-certain navigations)&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/dashboard"&lt;/span&gt; &lt;span class="na"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mount"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Dashboard&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Inertia v3 Additions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  No More Axios
&lt;/h3&gt;

&lt;p&gt;Inertia v3 ships its own built-in XHR client. Axios is no longer a required dependency, saving approximately 15 KB gzipped from your bundle. If you rely on Axios interceptors for token refresh or request logging, add Axios back as a peer dependency — but most projects do not need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  useHttp for Non-Navigation Requests
&lt;/h3&gt;

&lt;p&gt;Inertia v3 introduces &lt;code&gt;useHttp&lt;/code&gt; for HTTP calls that should not trigger page navigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useHttp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inertiajs/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SubscribeForm&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;processing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useHttp&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;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/subscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;processing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Subscribe&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;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;
  
  
  Optimistic Updates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inertiajs/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toggleTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/todos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;completed&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="na"&gt;optimistic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;completed&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="nx"&gt;t&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;// If the server returns a non-2xx response, the optimistic change is automatically rolled back&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ESM-Only Output
&lt;/h3&gt;

&lt;p&gt;Inertia v3 packages are ESM only. CommonJS &lt;code&gt;require('@inertiajs/react')&lt;/code&gt; will break. Ensure your Vite config does not force CJS output.&lt;/p&gt;




&lt;h2&gt;
  
  
  CSRF and Auth
&lt;/h2&gt;

&lt;p&gt;Inertia handles CSRF transparently when used with Laravel. The &lt;code&gt;HandleInertiaRequests&lt;/code&gt; middleware automatically shares the CSRF token, and Inertia's client attaches it to every request. You do not need to manually include &lt;code&gt;_token&lt;/code&gt; in form submissions.&lt;/p&gt;

&lt;p&gt;A token mismatch produces a &lt;code&gt;419&lt;/code&gt; response. Common cause: the session has expired between page load and form submission. Handle this in the Inertia response handler by redirecting to a re-login page.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Passing Eloquent models with too many fields.&lt;/strong&gt; Every prop you pass is serialized to JSON and embedded in the page. Use API resources or &lt;code&gt;-&amp;gt;only()&lt;/code&gt; to limit what leaves the server:&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;// Avoid&lt;/span&gt;
&lt;span class="s1"&gt;'product'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="c1"&gt;// Prefer&lt;/span&gt;
&lt;span class="s1"&gt;'product'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'slug'&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;Choosing Inertia for CRUD-heavy admin panels without React/Vue expertise.&lt;/strong&gt; Inertia adds a JavaScript build step, component structure, and client-side state management overhead. If your team is PHP-native and the UI is primarily forms and tables, Livewire 4 will ship faster with less complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting that Inertia v3 requires Laravel 11.&lt;/strong&gt; Upgrading the npm packages without checking the server adapter version will break the app silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;router.visit()&lt;/code&gt; for everything.&lt;/strong&gt; Inertia's &lt;code&gt;router.visit()&lt;/code&gt; triggers a full Inertia page visit including scroll reset and component remount. For in-page data mutations (toggle, delete, update), use &lt;code&gt;router.patch()&lt;/code&gt;, &lt;code&gt;router.put()&lt;/code&gt;, or &lt;code&gt;router.delete()&lt;/code&gt; — these preserve the current page component and merge updated props.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No shared data caching.&lt;/strong&gt; The &lt;code&gt;HandleInertiaRequests&lt;/code&gt; middleware &lt;code&gt;share()&lt;/code&gt; method runs on every request. Avoid expensive queries there. Cache shared data (authenticated user, permissions, navigation items) in the session or a short-lived cache.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Inertia Makes Sense (and When It Does Not)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Inertia when&lt;/th&gt;
&lt;th&gt;Prefer Livewire when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your team knows React or Vue&lt;/td&gt;
&lt;td&gt;Your team is primarily PHP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need complex client-side state&lt;/td&gt;
&lt;td&gt;The UI is mostly forms and lists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want the full npm ecosystem&lt;/td&gt;
&lt;td&gt;You want minimal JS tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You have rich data visualizations&lt;/td&gt;
&lt;td&gt;You are building an admin panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need SSR for public pages&lt;/td&gt;
&lt;td&gt;Server-round-trip latency is acceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Inertia is the "modern monolith" pattern: you keep Laravel's routing, auth, and data layer, but your frontend developers work in React or Vue with full HMR, TypeScript support, and component tooling — without maintaining a separate SPA deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verifying the Setup
&lt;/h2&gt;

&lt;p&gt;After scaffolding, confirm Inertia is wiring correctly:&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;# Run the Laravel dev server&lt;/span&gt;
php artisan serve

&lt;span class="c"&gt;# In a second terminal, run Vite&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the browser DevTools Network tab. Navigate between pages using &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; components. Subsequent navigations should show XHR requests returning &lt;code&gt;application/json&lt;/code&gt; with a &lt;code&gt;X-Inertia: true&lt;/code&gt; response header — not full HTML. If you see full HTML on every navigation, the &lt;code&gt;HandleInertiaRequests&lt;/code&gt; middleware is not registered.&lt;/p&gt;

&lt;p&gt;For automated testing, use Inertia's test helpers:&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;Inertia\Testing\AssertableInertia&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products index renders correctly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/products'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertInertia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Assert&lt;/span&gt; &lt;span class="nv"&gt;$page&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;$page&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Products/Index'&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;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products.data'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>react</category>
      <category>tutorial</category>
      <category>vue</category>
    </item>
    <item>
      <title>Finding and Fixing the Eloquent N+1 Problem</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Mon, 31 Aug 2026 10:01:07 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/finding-and-fixing-the-eloquent-n1-problem-1ke9</link>
      <guid>https://dev.to/mumbai_web_designer/finding-and-fixing-the-eloquent-n1-problem-1ke9</guid>
      <description>&lt;h1&gt;
  
  
  Finding and Fixing the Eloquent N+1 Problem
&lt;/h1&gt;

&lt;p&gt;You deploy a feature, and within days the DBA flags your page as a top query offender. A single request is issuing 101 queries where one would do. This is the N+1 problem — the most common performance mistake in Eloquent applications and, fortunately, one of the most straightforward to fix once you know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Laravel 10.x or higher (examples verified against Laravel 12 / PHP 8.2+)&lt;/li&gt;
&lt;li&gt;Familiarity with Eloquent models and basic relationships&lt;/li&gt;
&lt;li&gt;A local dev environment running&lt;/li&gt;
&lt;li&gt;Composer dev dependencies for detection tooling (details below)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What N+1 Actually Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;Consider a blog listing page. The controller loads posts and the Blade (or API response) iterates them:&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;// Controller&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Blade / resource loop&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;$posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;author&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="c1"&gt;// relationship access inside the loop&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eloquent lazy-loads &lt;code&gt;author&lt;/code&gt; on first access for each &lt;code&gt;$post&lt;/code&gt;. With 100 posts, that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 query: &lt;code&gt;SELECT * FROM posts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;100 queries: &lt;code&gt;SELECT * FROM users WHERE id = ?&lt;/code&gt; — one per post&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: &lt;strong&gt;101 queries&lt;/strong&gt;. The pattern scales linearly with the record count, which means it is invisible during local testing with five seed records and catastrophic in production with fifty thousand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Detect the Problem Before You Fix It
&lt;/h2&gt;

&lt;p&gt;Fix N+1 issues you can see. Three approaches, from lightest to most automated:&lt;/p&gt;

&lt;h3&gt;
  
  
  DB::enableQueryLog() — Zero-Dependency Inspection
&lt;/h3&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="no"&gt;Illuminate\Support\Facades\DB&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;enableQueryLog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&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;$posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;author&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="nv"&gt;$queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getQueryLog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;echo&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;$queries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 101 with 100 posts&lt;/span&gt;
&lt;span class="nf"&gt;dd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$queries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// inspect full SQL + bindings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the simplest approach and works in every environment with no package installation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model::preventLazyLoading() — Throw Early in Development
&lt;/h3&gt;

&lt;p&gt;Added to &lt;code&gt;app/Providers/AppServiceProvider.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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;preventLazyLoading&lt;/span&gt;&lt;span class="p"&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;isProduction&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;With this in place, any lazy relationship access throws a &lt;code&gt;LazyLoadingViolationException&lt;/code&gt; during local development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Illuminate\Database\LazyLoadingViolationException:
Attempted to lazy load [author] on model [App\Models\Post]
but lazy loading is disabled.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most effective way to catch N+1 issues before they ship. It does &lt;strong&gt;not&lt;/strong&gt; block lazy loading in production — that is intentional so that third-party packages that rely on lazy loading do not break. Complete your &lt;code&gt;with()&lt;/code&gt; calls and re-test; the exception disappears.&lt;/p&gt;

&lt;h3&gt;
  
  
  beyondcode/laravel-query-detector — Real-Time Auto-Detection
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;After installation and publishing the config (&lt;code&gt;php artisan vendor:publish --provider="BeyondCode\QueryDetector\QueryDetectorServiceProvider"&lt;/code&gt;), the package automatically detects N+1 patterns in real-time and can surface them via the browser console, Debugbar, or log file. This is the least invasive option for teams that want passive monitoring rather than hard exceptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Fix with Eager Loading
&lt;/h2&gt;

&lt;p&gt;Eager loading with &lt;code&gt;with()&lt;/code&gt; is the primary solution. It replaces the per-iteration lazy load with a single additional query, regardless of how many records the initial query returns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before — 101 queries for 100 posts&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// After — 2 queries total&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eloquent now runs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;SELECT * FROM posts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT * FROM users WHERE id IN (1, 2, 3, ...)&lt;/code&gt; — all author IDs in one &lt;code&gt;IN&lt;/code&gt; clause&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Nested Relationships
&lt;/h3&gt;

&lt;p&gt;Chain dot-notation to eager load through multiple levels:&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;// Loads posts → comments → comment authors in 3 total queries&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'comments.author'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Constrained Eager Loading
&lt;/h3&gt;

&lt;p&gt;Filter the eager-loaded relationship using a closure:&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;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'comments'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;$query&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;'approved'&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'desc'&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;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// $post-&amp;gt;comments only contains approved comments, still in 2 queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Column Selection — Avoid SELECT * Overhead
&lt;/h3&gt;

&lt;p&gt;When eager loading, always specify only the columns you actually need. This is particularly important when the related model has large text or JSON columns:&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;// Bad — loads all user columns for every post (including password hash, settings JSON, etc.)&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Good — only the columns the view uses&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'author:id,name,avatar_url'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical rule:&lt;/strong&gt; When constraining eager-loaded columns using the colon syntax, you must always include both the primary key (&lt;code&gt;id&lt;/code&gt;) and the foreign key (&lt;code&gt;user_id&lt;/code&gt; on the parent). If you omit the primary key on the relation, Eloquent cannot match related records back to their parents, and &lt;code&gt;$post-&amp;gt;author&lt;/code&gt; will always be &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Always-Eager Relationships with $with
&lt;/h2&gt;

&lt;p&gt;If a relationship is needed on virtually every query for a model, define it on the model 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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&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="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$with&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'author'&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 &lt;code&gt;Post::query()&lt;/code&gt; now eager loads &lt;code&gt;author&lt;/code&gt; automatically. Override on a per-query basis with &lt;code&gt;without()&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;// Suppresses the automatic eager load for this query only&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;without&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;$with&lt;/code&gt; judiciously. If a relationship is only needed in a few contexts, &lt;code&gt;$with&lt;/code&gt; adds unnecessary JOIN/subquery overhead to every other query. Prefer explicit &lt;code&gt;with()&lt;/code&gt; at the call site unless the relationship is genuinely used in the majority of code paths.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Variant: The Hidden N+1 Inside a Loop Condition
&lt;/h2&gt;

&lt;p&gt;N+1 does not always look like &lt;code&gt;$post-&amp;gt;author-&amp;gt;name&lt;/code&gt;. It can hide inside conditionals and method 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="c1"&gt;// Still N+1 — accessing relationship inside an if&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;$posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&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;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// render tagged post differently&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Fix — eager load tags&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&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;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another common variant: loading one relationship with &lt;code&gt;with()&lt;/code&gt; but accessing a different, non-eager-loaded relationship inside the same loop:&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;// Only 'author' is eager loaded — accessing 'category' still causes N+1&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;category&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="c1"&gt;// N+1 on category&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Fix — eager load both&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  When Boolean Presence Is All You Need: withExists()
&lt;/h2&gt;

&lt;p&gt;A common pattern is checking whether a relationship exists rather than loading its data. Using &lt;code&gt;withCount()&lt;/code&gt; for this works but fires a &lt;code&gt;COUNT(*)&lt;/code&gt; subquery for each row. &lt;code&gt;withExists()&lt;/code&gt; is more efficient — it generates a cheaper &lt;code&gt;EXISTS&lt;/code&gt; subquery instead:&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;// withCount — generates COUNT(*) subquery, returns integer&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'comments'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// $post-&amp;gt;comments_count === 5&lt;/span&gt;

&lt;span class="c1"&gt;// withExists — generates EXISTS subquery, returns boolean&lt;/span&gt;
&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'comments'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// $post-&amp;gt;comments_exists === true&lt;/span&gt;
&lt;span class="c1"&gt;// Use this when you only need to know if comments exist, not how many&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both run as subqueries within the main &lt;code&gt;SELECT&lt;/code&gt;, not as separate queries. Pick &lt;code&gt;withExists()&lt;/code&gt; whenever the count itself is not needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Large Dataset Caveat: cursor() Does Not Support with()
&lt;/h2&gt;

&lt;p&gt;For iterating large result sets, Laravel offers &lt;code&gt;cursor()&lt;/code&gt;, which uses a single unbuffered query and hydrates one model at a time — approximately 1.87 MB for 300,000 rows versus batch-size times model-size for &lt;code&gt;chunk()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;cursor()&lt;/code&gt; does not support eager loading with &lt;code&gt;with()&lt;/code&gt;. Chaining them does not throw an error, but relationship access inside the loop is lazy-loaded, which puts you squarely back in N+1 territory:&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;// WRONG — cursor() + relationship access = N+1, no error thrown&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;cursor&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;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;author&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="c1"&gt;// still N+1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Use chunk() instead when you need eager loading on large datasets&lt;/span&gt;
&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&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;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$posts&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;$posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;author&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="c1"&gt;// safe — author is eager loaded per chunk&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If memory usage is critical and you genuinely cannot use &lt;code&gt;chunk()&lt;/code&gt;, restructure the loop to avoid relationship access — preload the related data as a keyed collection before the cursor loop:&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;// Preload all authors as a lookup map&lt;/span&gt;
&lt;span class="nv"&gt;$authors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;pluck&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;'id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// keyed by user_id&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;cursor&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;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$authors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// O(1) array lookup, no DB query&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing That You Actually Fixed It
&lt;/h2&gt;

&lt;p&gt;Do not rely solely on visual confirmation. Write a feature test that asserts the query count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_post_listing_does_not_cause_n_plus_1&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;// Create 10 posts each with an author&lt;/span&gt;
    &lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sequence&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;$seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$seq&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="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;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$queryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="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="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$queryCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$queryCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Simulate the controller&lt;/span&gt;
    &lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;author&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="c1"&gt;// Should be exactly 2 queries: one for posts, one for authors&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$queryCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test will fail if someone later removes &lt;code&gt;with('author')&lt;/code&gt; and reintroduces N+1. Add it to CI and it becomes a permanent guard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tradeoffs and Limitations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Eager loading vs lazy loading — memory cost.&lt;/strong&gt; Eager loading trades multiple small queries for a single larger one. If the related dataset is huge (e.g., a post with 50,000 comments), eager loading all of them into memory is worse than lazy loading. Use constrained eager loading with &lt;code&gt;limit()&lt;/code&gt; or paginate the relationship in those cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;preventLazyLoading() in production.&lt;/strong&gt; The guard only activates when &lt;code&gt;!app()-&amp;gt;isProduction()&lt;/code&gt; by default. Running it unconditionally in production may break third-party packages that use lazy loading — keep it development-only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;withExists() and withCount() — subquery cost.&lt;/strong&gt; Both add a correlated subquery per row to the main &lt;code&gt;SELECT&lt;/code&gt;. For large tables, ensure foreign key columns are indexed or these subqueries can be slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;$with on models.&lt;/strong&gt; Always-eager relationships add overhead to every query. If a relationship is only needed in a minority of code paths, prefer explicit &lt;code&gt;with()&lt;/code&gt; at the call site.&lt;/p&gt;




&lt;p&gt;For a broader look at query optimization in Laravel — covering &lt;code&gt;upsert()&lt;/code&gt; for bulk writes, aggregate subqueries, large-dataset strategies, and vector search in Laravel 13 — see &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-eloquent-database-optimization" rel="noopener noreferrer"&gt;Eloquent and Database Optimization: Relationships, N+1, Upserts, and Vector Search&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>eloquent</category>
      <category>performance</category>
    </item>
    <item>
      <title>Documenting Laravel APIs with Scramble</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Mon, 31 Aug 2026 05:36:18 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/documenting-laravel-apis-with-scramble-9f</link>
      <guid>https://dev.to/mumbai_web_designer/documenting-laravel-apis-with-scramble-9f</guid>
      <description>&lt;h1&gt;
  
  
  Documenting Laravel APIs with Scramble
&lt;/h1&gt;

&lt;p&gt;If you have ever shipped a Laravel API and watched a frontend developer open Postman to reverse-engineer your endpoints, you know the pain of missing documentation. The traditional solution—Swagger annotations—requires you to decorate every controller method with dozens of lines of PHPDoc that drift out of sync the moment someone renames a parameter. Scramble takes a different approach: it reads your existing code and generates an OpenAPI 3.1 spec automatically, with zero annotations required.&lt;/p&gt;

&lt;p&gt;This article walks through installing Scramble on a Laravel 11 or 12 project, what it infers automatically, how to fill in the gaps, and the tradeoffs you should know before adopting it in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Laravel 11 or 12, PHP 8.2+, Composer. If you have not scaffolded your API routes yet, run &lt;code&gt;php artisan install:api&lt;/code&gt; first—Laravel 11/12 no longer ships &lt;code&gt;routes/api.php&lt;/code&gt; by default. See &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-api-guide" rel="noopener noreferrer"&gt;Building Production-Ready APIs with Laravel&lt;/a&gt; for the full setup walkthrough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Scramble Instead of Swagger Annotations
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;darkaonline/l5-swagger&lt;/code&gt; package (and its predecessor &lt;code&gt;swagger-php&lt;/code&gt;) requires PHPDoc blocks like this on every endpoint:&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="cd"&gt;/**
 * @OA\Get(
 *     path="/api/v1/posts",
 *     summary="List posts",
 *     tags={"Posts"},
 *     @OA\Parameter(name="page", in="query", ..."),
 *     @OA\Response(response=200, description="Success", ...)
 * )
 */&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AnonymousResourceCollection&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That annotation block is longer than many controller methods. It also does not tell you when the annotation is wrong—if you change the response shape but forget to update &lt;code&gt;@OA\Response&lt;/code&gt;, nobody finds out until a consumer hits a 500.&lt;/p&gt;

&lt;p&gt;Scramble instead parses your controller return types, FormRequest rules, Eloquent API Resources, and route definitions. If your code is typed correctly, the documentation is correct by construction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require dedoc/scramble
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scramble auto-discovers itself via Laravel's package discovery. No service provider registration is needed. After installation, two routes are available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/docs/api&lt;/code&gt; — Interactive Stoplight Elements UI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/docs/api.json&lt;/code&gt; — Raw OpenAPI 3.1 JSON spec&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both routes are restricted to the &lt;code&gt;local&lt;/code&gt; environment by default. To expose them in staging or production, publish the config and adjust the &lt;code&gt;middleware&lt;/code&gt; array:&lt;br&gt;
&lt;/p&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;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Dedoc&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="s2"&gt;cramble&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="s2"&gt;crambleServiceProvider"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates &lt;code&gt;config/scramble.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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'api_path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'api_domain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'info'&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;'version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'API_VERSION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1.0.0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="s1"&gt;'middleware'&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;'web'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;RestrictedDocsAccess&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;// ships with Scramble&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'extensions'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To restrict docs to authenticated users in production, replace &lt;code&gt;RestrictedDocsAccess&lt;/code&gt; with your own middleware or gate check.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Scramble Infers Automatically
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Route Parameters
&lt;/h3&gt;

&lt;p&gt;Given a route like &lt;code&gt;Route::get('/posts/{post}', [PostController::class, 'show'])&lt;/code&gt;, Scramble reads the route binding and produces the correct &lt;code&gt;{post}&lt;/code&gt; path parameter in the spec. If you use model binding (&lt;code&gt;public function show(Post $post)&lt;/code&gt;), it infers the parameter type from the model's primary key type.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request Body from FormRequest
&lt;/h3&gt;

&lt;p&gt;Scramble reads &lt;code&gt;rules()&lt;/code&gt; from a FormRequest and maps Laravel validation rules to JSON Schema types:&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;StorePostRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormRequest&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;rules&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;'title'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:255'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'content'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'status'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'in:draft,published'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'tags'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'tags.*'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:50'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scramble converts &lt;code&gt;in:draft,published&lt;/code&gt; to an OpenAPI &lt;code&gt;enum&lt;/code&gt;, infers &lt;code&gt;tags&lt;/code&gt; as an array of strings, and marks &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, and &lt;code&gt;status&lt;/code&gt; as required. You get accurate request body documentation without writing a single annotation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Response Shape from API Resources
&lt;/h3&gt;

&lt;p&gt;When a controller method returns a typed Resource or ResourceCollection, Scramble resolves the return type and inspects the &lt;code&gt;toArray()&lt;/code&gt; 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="c1"&gt;// app/Http/Resources/PostResource.php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JsonResource&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;toArray&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="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'id'&lt;/span&gt;             &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&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;'title'&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;title&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'author'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserResource&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;whenLoaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="s1"&gt;'comments_count'&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;whenCounted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'comments'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'created_at'&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;created_at&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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;Scramble picks up the field names and their types. It handles &lt;code&gt;whenLoaded()&lt;/code&gt; by marking the nested resource as nullable/optional, which is accurate—the field only appears when the relationship is eager-loaded.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP Status Codes
&lt;/h3&gt;

&lt;p&gt;Scramble infers the success status code from what the controller returns. A &lt;code&gt;response()-&amp;gt;json([], 201)&lt;/code&gt; produces a &lt;code&gt;201&lt;/code&gt; in the spec. Validation failures via FormRequest automatically produce a &lt;code&gt;422&lt;/code&gt; response schema showing the standard Laravel error envelope.&lt;/p&gt;




&lt;h2&gt;
  
  
  Filling the Gaps with Attributes
&lt;/h2&gt;

&lt;p&gt;Scramble cannot infer everything from code structure alone. For cases where inference falls short, it provides PHP 8 attributes instead of PHPDoc annotations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Describe an Endpoint
&lt;/h3&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;Dedoc\Scramble\Attributes\OperationId&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;Dedoc\Scramble\Attributes\Summary&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;Dedoc\Scramble\Attributes\Description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Summary('Create a new post')]&lt;/span&gt;
&lt;span class="na"&gt;#[Description('Stores a draft or published post. Requires the `posts.create` permission.')]&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;StorePostRequest&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;PostResource&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&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;
  
  
  Document Query Parameters
&lt;/h3&gt;

&lt;p&gt;Query parameters that are not in a FormRequest (filters, sort options, cursor values) need explicit documentation:&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;Dedoc\Scramble\Attributes\QueryParameter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[QueryParameter('sort', description: 'Sort field. Allowed: created_at, title.', type: 'string', example: 'created_at')]&lt;/span&gt;
&lt;span class="na"&gt;#[QueryParameter('direction', description: 'Sort direction.', enum: ['asc', 'desc'], default: 'desc')]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;AnonymousResourceCollection&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'comments'&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;orderBy&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;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'created_at'&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;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'direction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'desc'&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;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$posts&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;
  
  
  Exclude Internal Endpoints
&lt;/h3&gt;

&lt;p&gt;Not every route belongs in public documentation. Tag internal or admin-only endpoints to exclude them:&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;Dedoc\Scramble\Attributes\ExcludeFromDocs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[ExcludeFromDocs]&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;internalMetrics&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Versioned APIs
&lt;/h2&gt;

&lt;p&gt;If you are running versioned route groups (the recommended approach for production APIs), configure Scramble to point at a specific API prefix:&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/scramble.php&lt;/span&gt;
&lt;span class="s1"&gt;'api_path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'api/v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For multiple versions with separate documentation UIs, register additional Scramble instances in a service provider:&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;// app/Providers/AppServiceProvider.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dedoc\Scramble\Scramble&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;Dedoc\Scramble\Support\Generator\OpenApi&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;Dedoc\Scramble\Support\Generator\SecurityScheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Scramble&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;registerApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'api_path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'api/v2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'info'&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;'version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2.0.0'&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 produces separate docs at &lt;code&gt;/docs/v2&lt;/code&gt; without conflating the two versions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Authentication in the Spec
&lt;/h2&gt;

&lt;p&gt;Scramble does not automatically detect that your routes are behind &lt;code&gt;auth:sanctum&lt;/code&gt;. Register the security scheme manually:&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;// app/Providers/AppServiceProvider.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dedoc\Scramble\Scramble&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;Dedoc\Scramble\Support\Generator\OpenApi&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;Dedoc\Scramble\Support\Generator\SecurityScheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Scramble&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;configure&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;withDocumentTransformers&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;OpenApi&lt;/span&gt; &lt;span class="nv"&gt;$openApi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$openApi&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;SecurityScheme&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;http&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bearer'&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 adds a &lt;code&gt;BearerAuth&lt;/code&gt; security requirement globally. If some routes are public, apply &lt;code&gt;-&amp;gt;withoutSecurity()&lt;/code&gt; per-route using the attribute approach.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exporting the Spec for CI and Postman
&lt;/h2&gt;

&lt;p&gt;Generate a static JSON spec on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan scramble:export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes &lt;code&gt;storage/api-docs/api.json&lt;/code&gt; by default. You can commit this file and fail the CI build if it changes unexpectedly, which forces developers to update documentation alongside code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/api-docs.yml (pseudocode)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php artisan scramble:export&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git diff --exit-code storage/api-docs/api.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import &lt;code&gt;storage/api-docs/api.json&lt;/code&gt; directly into Postman using &lt;strong&gt;File &amp;gt; Import&lt;/strong&gt; to generate a full collection with examples.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations and Tradeoffs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What Scramble does not infer well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic response shapes built with conditional logic inside &lt;code&gt;toArray()&lt;/code&gt; beyond &lt;code&gt;whenLoaded()&lt;/code&gt; and &lt;code&gt;whenCounted()&lt;/code&gt;. If you use &lt;code&gt;$this-&amp;gt;when(someCondition(), ...)&lt;/code&gt; with complex expressions, the inferred schema may be incomplete.&lt;/li&gt;
&lt;li&gt;Endpoints that return raw &lt;code&gt;response()-&amp;gt;json([...])&lt;/code&gt; with an inline array instead of a typed Resource. Scramble cannot inspect an anonymous array literal for types.&lt;/li&gt;
&lt;li&gt;Polymorphic relationships inside Resources. Scramble cannot resolve which of several model types might appear inside a &lt;code&gt;morphTo()&lt;/code&gt; relationship without a hint.&lt;/li&gt;
&lt;li&gt;Custom exception handlers that return non-standard JSON envelopes. If you override &lt;code&gt;Handler::render()&lt;/code&gt;, Scramble may not know about your custom error shape.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Annotation fatigue is not zero.&lt;/strong&gt; Scramble eliminates most annotations but not all. Query parameters, custom response descriptions, and security declarations still require attributes or document transformers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The docs UI is not customisable without overriding the view.&lt;/strong&gt; If your team requires Swagger UI instead of Stoplight Elements, you will need to point a separate Swagger UI instance at the generated JSON endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scramble vs. l5-swagger:&lt;/strong&gt; Scramble wins on maintenance cost (no annotation drift), accuracy (spec is derived from running code), and setup speed. l5-swagger wins when you need fine-grained control over every schema detail or are working with a legacy codebase that lacks return-type declarations and FormRequest classes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verifying Your Documentation
&lt;/h2&gt;

&lt;p&gt;After setup, open &lt;code&gt;/docs/api&lt;/code&gt; in your browser and cross-check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every route in &lt;code&gt;routes/api.php&lt;/code&gt; appears in the sidebar.&lt;/li&gt;
&lt;li&gt;FormRequest fields match what the endpoint actually accepts.&lt;/li&gt;
&lt;li&gt;Resource fields match the database columns and casts.&lt;/li&gt;
&lt;li&gt;Status codes are correct (201 for creation, 204 for deletion, 422 for validation).&lt;/li&gt;
&lt;li&gt;Bearer auth appears on protected routes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For automated verification, use Spectral to lint the exported spec against the OpenAPI 3.1 ruleset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @stoplight/spectral-cli lint storage/api-docs/api.json &lt;span class="nt"&gt;--ruleset&lt;/span&gt; @stoplight/spectral-owasp-ruleset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OWASP ruleset flags common API security issues (missing authentication declarations, overly permissive schemas) directly from the spec file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not typing controller return values.&lt;/strong&gt; Scramble relies on PHP return type declarations. &lt;code&gt;public function index()&lt;/code&gt; without a return type yields an empty response schema. Add &lt;code&gt;AnonymousResourceCollection&lt;/code&gt; or &lt;code&gt;PostResource&lt;/code&gt; return types everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;response()-&amp;gt;json($data)&lt;/code&gt; instead of Resources.&lt;/strong&gt; Once you bypass the Resource layer, Scramble cannot inspect the shape. Reserve raw JSON responses for simple cases and document them with attributes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposing the docs route in production without authentication.&lt;/strong&gt; The default &lt;code&gt;RestrictedDocsAccess&lt;/code&gt; middleware only allows local access. Wrap the docs routes with &lt;code&gt;auth:sanctum&lt;/code&gt; or an IP allowlist before staging deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to re-export after schema changes.&lt;/strong&gt; If your CI pipeline imports a stale spec into Postman or a mock server, consumers test against outdated contracts. Automate &lt;code&gt;php artisan scramble:export&lt;/code&gt; in your deployment pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running Scramble on routes that include Livewire or Inertia endpoints.&lt;/strong&gt; Set &lt;code&gt;api_path&lt;/code&gt; precisely to your API prefix so Scramble does not attempt to parse server-rendered page routes.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Scramble is the lowest-friction path to accurate OpenAPI documentation for Laravel APIs. It will not eliminate every annotation, but it eliminates the most tedious ones—the ones that duplicate information already present in your types, FormRequests, and Resource classes.&lt;/p&gt;

&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Filtering Laravel APIs with Spatie Query Builder</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Mon, 31 Aug 2026 04:41:49 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/filtering-laravel-apis-with-spatie-query-builder-5b8k</link>
      <guid>https://dev.to/mumbai_web_designer/filtering-laravel-apis-with-spatie-query-builder-5b8k</guid>
      <description>&lt;p&gt;If you have spent any time building Laravel APIs, you have almost certainly written a controller method that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&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;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&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;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&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;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author_id'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="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;'author_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&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;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sort'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="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;orderBy&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="n"&gt;sort&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'direction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'asc'&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;PostResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collection&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;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for two filters. At ten filters it becomes a maintenance problem. At twenty, it's a bug surface. &lt;strong&gt;Spatie Laravel Query Builder&lt;/strong&gt; replaces that entire pattern with a declarative, tested, composable API that scales cleanly.&lt;/p&gt;

&lt;p&gt;This article focuses narrowly on Spatie Query Builder — installation through production patterns. For the broader context of Laravel API architecture (versioning, Sanctum auth, rate limiting, N+1 prevention), see &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-api-guide" rel="noopener noreferrer"&gt;Building Production-Ready APIs with Laravel&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 11 or 12&lt;/strong&gt; (PHP 8.2 minimum)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spatie/laravel-query-builder&lt;/code&gt; &lt;strong&gt;^6.x&lt;/strong&gt; (supports Laravel 11/12)&lt;/li&gt;
&lt;li&gt;Composer installed&lt;/li&gt;
&lt;li&gt;Basic familiarity with Eloquent and API Resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install the package:&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 spatie/laravel-query-builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No service provider registration is required — the package auto-discovers itself.&lt;/p&gt;




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

&lt;p&gt;Spatie Query Builder wraps an Eloquent query and maps incoming HTTP query parameters to allowed filters, sorts, and includes. Only parameters you explicitly allow are applied. Everything else is silently ignored, which prevents query-injection style attacks where a client passes arbitrary column names.&lt;/p&gt;

&lt;p&gt;The basic structure:&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;Spatie\QueryBuilder\QueryBuilder&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;Spatie\QueryBuilder\AllowedFilter&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;Spatie\QueryBuilder\AllowedSort&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;Spatie\QueryBuilder\AllowedInclude&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowedFilters&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowedSorts&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowedIncludes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client controls the query via URL parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/v1/posts?filter[status]=published&amp;amp;sort=-created_at&amp;amp;include=author
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Filters in Depth
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Exact Filters
&lt;/h3&gt;

&lt;p&gt;The simplest filter matches a column exactly:&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;allowedFilters&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'author_id'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request to &lt;code&gt;?filter[status]=published&lt;/code&gt; translates to &lt;code&gt;WHERE status = 'published'&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partial (LIKE) Filters
&lt;/h3&gt;

&lt;p&gt;For search-style filtering:&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;Spatie\QueryBuilder\AllowedFilter&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;allowedFilters&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nc"&gt;AllowedFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'title'&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;?filter[title]=laravel&lt;/code&gt; becomes &lt;code&gt;WHERE title LIKE '%laravel%'&lt;/code&gt;. Be aware this disables index usage on most database engines — add a full-text index or move to a search engine (Meilisearch, Typesense) if the table is large.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scope Filters
&lt;/h3&gt;

&lt;p&gt;Scope filters delegate the filtering logic to a named Eloquent scope on the model:&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;// Post 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;scopePublishedAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Builder&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;$query&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;'published_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Controller&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowedFilters&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nc"&gt;AllowedFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'published_after'&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;Request: &lt;code&gt;?filter[published_after]=2026-01-01&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Scope filters are the cleanest way to encapsulate complex WHERE logic (date ranges, geographic bounding boxes, status machines) without polluting the controller.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Filters
&lt;/h3&gt;

&lt;p&gt;When you need full control — joining another table, conditional subqueries, or multi-column logic — implement &lt;code&gt;\Spatie\QueryBuilder\Filters\Filter&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Spatie\QueryBuilder\Filters\Filter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TagFilter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Filter&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;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="nv"&gt;$value&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;$property&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;$tags&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;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;explode&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;$value&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;whereHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tags'&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;Builder&lt;/span&gt; &lt;span class="nv"&gt;$q&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;$tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&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;whereIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tags&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="c1"&gt;// Controller&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowedFilters&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nc"&gt;AllowedFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tags'&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;TagFilter&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;Request: &lt;code&gt;?filter[tags]=php,laravel&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Filter Defaults
&lt;/h3&gt;

&lt;p&gt;You can supply a default value so that the filter applies even when the client does not pass it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;AllowedFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for endpoints that should only surface active records by default, with an opt-in to see all records for admin consumers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sorting
&lt;/h2&gt;

&lt;p&gt;Allowed sorts map request parameter values to column names:&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;allowedSorts&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;AllowedSort&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'newest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client prefixes a column name with &lt;code&gt;-&lt;/code&gt; for descending order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?sort=-created_at        → ORDER BY created_at DESC
?sort=title              → ORDER BY title ASC
?sort=newest             → ORDER BY created_at ASC (aliased)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set a default sort so paginated responses are stable without a client-supplied sort:&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;defaultSort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'-created_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a default sort, &lt;code&gt;paginate()&lt;/code&gt; on a large dataset produces non-deterministic page boundaries — records can appear on multiple pages or be skipped entirely as rows are inserted.&lt;/p&gt;




&lt;h2&gt;
  
  
  Eager-Loading Includes
&lt;/h2&gt;

&lt;p&gt;Allowed includes let the client opt into relationship loading:&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;allowedIncludes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'comments.author'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request: &lt;code&gt;?include=author,tags&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Behind the scenes this calls &lt;code&gt;with(['author', 'tags'])&lt;/code&gt; on the query. Because the relationships are eager-loaded before the Resource layer runs, &lt;code&gt;whenLoaded()&lt;/code&gt; in your API Resource works correctly:&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;// PostResource.php&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;toArray&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="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&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;'title'&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;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'author'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserResource&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;whenLoaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="s1"&gt;'tags'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;TagResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collection&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;whenLoaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tags'&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;When &lt;code&gt;author&lt;/code&gt; is not included, &lt;code&gt;whenLoaded('author')&lt;/code&gt; returns &lt;code&gt;MissingValue&lt;/code&gt; and the key is omitted from the response. No N+1. No accidental data exposure.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Full Controller Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers\Api\V1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Resources\PostResource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Post&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\Resources\Json\AnonymousResourceCollection&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;Spatie\QueryBuilder\AllowedFilter&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;Spatie\QueryBuilder\AllowedSort&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;Spatie\QueryBuilder\QueryBuilder&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;PostController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AnonymousResourceCollection&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowedFilters&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="nc"&gt;AllowedFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nc"&gt;AllowedFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author_id'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nc"&gt;AllowedFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nc"&gt;AllowedFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'published_after'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nc"&gt;AllowedFilter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tags'&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;TagFilter&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;allowedSorts&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;AllowedSort&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'newest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;defaultSort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'-created_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;allowedIncludes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tags'&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;paginate&lt;/span&gt;&lt;span class="p"&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;integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'per_page'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$posts&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 replaces 50+ lines of imperative if-blocks with a structure that is readable, testable in isolation, and extendable by adding a single line.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Using &lt;code&gt;AllowedFilter::partial()&lt;/code&gt; on unindexed columns&lt;/strong&gt;&lt;br&gt;
LIKE queries with a leading wildcard (&lt;code&gt;%term%&lt;/code&gt;) cannot use standard B-Tree indexes. For any table above a few thousand rows, either add a full-text index, limit the search to a prefix pattern (&lt;code&gt;term%&lt;/code&gt;), or offload to a dedicated search service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Allowing includes without thinking about depth&lt;/strong&gt;&lt;br&gt;
Nested includes like &lt;code&gt;comments.author.profile&lt;/code&gt; can generate deep join trees. Audit every allowed include path — or cap depth using the package's &lt;code&gt;allowedIncludes&lt;/code&gt; list and never use wildcards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Omitting &lt;code&gt;defaultSort()&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Pagination without a stable sort order is broken by definition. Always set a default sort, ideally on an indexed column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Filtering on non-guarded columns in custom filters&lt;/strong&gt;&lt;br&gt;
Custom &lt;code&gt;Filter&lt;/code&gt; implementations receive the raw client-supplied value. Always validate or cast inside the implementation. Do not interpolate &lt;code&gt;$value&lt;/code&gt; into raw SQL strings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Not wrapping the QueryBuilder in a FormRequest&lt;/strong&gt;&lt;br&gt;
QueryBuilder handles the query-parameter side. It does not validate that &lt;code&gt;filter[author_id]&lt;/code&gt; is an integer or that &lt;code&gt;filter[published_after]&lt;/code&gt; is a valid date. Add a FormRequest alongside the QueryBuilder for input validation:&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;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;IndexPostRequest&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;AnonymousResourceCollection&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// $request-&amp;gt;validated() runs before QueryBuilder reads the parameters&lt;/span&gt;
    &lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Limitations and Tradeoffs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Spatie Query Builder works on Eloquent queries.&lt;/strong&gt; It does not integrate with raw &lt;code&gt;DB::select()&lt;/code&gt; calls or Eloquent queries that have already been executed. If your endpoint returns data from a stored procedure or a complex multi-union query, you will need to handle filtering manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;filter[]&lt;/code&gt; bracket syntax&lt;/strong&gt; is conventional for this library and matches PHP's native query-string parsing. However, some API clients and gateways (AWS API Gateway URL validation, certain proxies) require additional configuration to allow bracket characters in query parameters. Test your infrastructure with this syntax early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sorting by computed or aggregate columns&lt;/strong&gt; (e.g., &lt;code&gt;sort=comments_count&lt;/code&gt;) requires that the column is already selected or appended via &lt;code&gt;withCount()&lt;/code&gt; on the base query before handing control to QueryBuilder. Use &lt;code&gt;AllowedSort::field()&lt;/code&gt; to alias the aggregate:&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;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'comments'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// must be in the base 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;allowedSorts&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nc"&gt;AllowedSort&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'popularity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'comments_count'&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;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Filtered Endpoints
&lt;/h2&gt;

&lt;p&gt;Feature tests against filtered endpoints are straightforward with Laravel's test helpers:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'filters posts by status'&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;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanctum'&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;getJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/posts?filter[status]=draft'&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;assertOk&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;assertJsonCount&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="s1"&gt;'data'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertJsonPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'data.0.status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ignores unknown filters'&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;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&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;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="c1"&gt;// 'unknown_column' is not in allowedFilters — should be ignored, not error&lt;/span&gt;
    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanctum'&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;getJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/posts?filter[unknown_column]=value'&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;assertOk&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;assertJsonCount&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="s1"&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;By default Spatie Query Builder ignores disallowed filters silently. If you want it to throw a &lt;code&gt;422&lt;/code&gt; when an unknown filter is passed (useful for strict API clients), set &lt;code&gt;'throw_invalid_query_exceptions' =&amp;gt; true&lt;/code&gt; in the &lt;code&gt;config/query-builder.php&lt;/code&gt; file (publish it first with &lt;code&gt;php artisan vendor:publish --provider="Spatie\QueryBuilder\QueryBuilderServiceProvider"&lt;/code&gt;).&lt;/p&gt;




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

&lt;p&gt;Spatie Query Builder solves a real, recurring Laravel API problem — complex, hand-rolled filtering logic — through a clean declarative interface. The key patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;exact&lt;/strong&gt; and &lt;strong&gt;scope&lt;/strong&gt; filters for most cases; reserve &lt;strong&gt;custom&lt;/strong&gt; filters for complex multi-table logic&lt;/li&gt;
&lt;li&gt;Always set a &lt;strong&gt;default sort&lt;/strong&gt; on an indexed column&lt;/li&gt;
&lt;li&gt;Combine &lt;code&gt;allowedIncludes&lt;/code&gt; with &lt;code&gt;whenLoaded()&lt;/code&gt; in API Resources to avoid N+1 queries&lt;/li&gt;
&lt;li&gt;Validate raw input in a &lt;strong&gt;FormRequest&lt;/strong&gt; — QueryBuilder applies filters but does not type-check them&lt;/li&gt;
&lt;li&gt;Publish the config and enable &lt;code&gt;throw_invalid_query_exceptions&lt;/code&gt; in strict consumer environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>Building a Production REST API with Laravel</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Fri, 28 Aug 2026 10:00:50 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/building-a-production-rest-api-with-laravel-585b</link>
      <guid>https://dev.to/mumbai_web_designer/building-a-production-rest-api-with-laravel-585b</guid>
      <description>&lt;h1&gt;
  
  
  Building a Production REST API with Laravel
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; PHP 8.2+, Composer, Laravel 12.x, Redis (for production rate limiting and queues), basic familiarity with Eloquent and routing.&lt;/p&gt;

&lt;p&gt;Laravel 12 (released March 2025) is a maintenance release on top of the structural changes introduced in Laravel 11. If you are starting a new API project today, the setup steps are meaningfully different from what you may remember from Laravel 9 or 10. This article walks through the decisions and implementation steps that matter most when shipping a REST API to production — authentication strategy, resource transformation, rate limiting, CORS, versioning, and testing — without rehashing the basics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Scaffold the API Layer
&lt;/h2&gt;

&lt;p&gt;In Laravel 11 and 12, &lt;code&gt;routes/api.php&lt;/code&gt; is &lt;strong&gt;not present&lt;/strong&gt; in a fresh installation. Running &lt;code&gt;php artisan install:api&lt;/code&gt; creates the file, installs Sanctum, registers the &lt;code&gt;throttle:api&lt;/code&gt; middleware on the &lt;code&gt;api&lt;/code&gt; route group, and adds the &lt;code&gt;EnsureFrontendRequestsAreStateful&lt;/code&gt; middleware.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan &lt;span class="nb"&gt;install&lt;/span&gt;:api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you genuinely need a full OAuth2 server — authorization code flows, client credentials for machine-to-machine, or token introspection for third-party developers — swap in Passport:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan &lt;span class="nb"&gt;install&lt;/span&gt;:api &lt;span class="nt"&gt;--passport&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the vast majority of projects (own SPA, own mobile app, internal microservice), Sanctum is the correct choice. Passport adds migrations, encryption key management, and client administration overhead that you do not need for first-party consumers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; If you are on Passport 13.0.0–13.7.0, upgrade to 13.7.1+ immediately. CVE-2026-39976 (CVSS 7.1) allows a &lt;code&gt;client_credentials&lt;/code&gt; JWT token to authenticate as a real user when the client ID integer matches a user's ID. See the &lt;a href="https://github.com/laravel/passport/security/advisories/GHSA-349c-2h2f-mxf6" rel="noopener noreferrer"&gt;official advisory&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2: Version Your Routes from Day One
&lt;/h2&gt;

&lt;p&gt;Retrofitting versioning after launch means coordinating client updates and maintaining dual routing indefinitely. The URL prefix strategy (&lt;code&gt;/api/v1/&lt;/code&gt;, &lt;code&gt;/api/v2/&lt;/code&gt;) is the most debuggable and cache-friendly approach.&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;// routes/api.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v1'&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="nf"&gt;base_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'routes/api_v1.php'&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;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v2'&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="nf"&gt;base_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'routes/api_v2.php'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep separate controller namespaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;App\Http\Controllers\Api\V1\PostController&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;App\Http\Controllers\Api\V2\PostController&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And separate Resource classes per version so V1 and V2 response shapes evolve independently. Sharing a Resource class across versions is a false economy — when V2 requires a renamed field, you will end up with conditionals that are harder to maintain than two clean files.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Protect Routes with Sanctum
&lt;/h2&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_v1.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\Api\V1\PostController&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\Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth:sanctum'&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;apiResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PostController&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;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$r&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;$r&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For browser-based SPAs, use Sanctum's stateful (HttpOnly cookie) authentication — tokens stored in &lt;code&gt;localStorage&lt;/code&gt; are vulnerable to XSS exfiltration. For mobile apps and CLI clients, use Sanctum API tokens stored in the device's secure keychain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Transform Responses with API Resources
&lt;/h2&gt;

&lt;p&gt;Returning raw Eloquent models from controllers leaks internal field names, exposes timestamps in inconsistent formats, and makes it painful to add computed fields later. API Resources are the standard transformation layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:resource PostResource
&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;// app/Http/Resources/V1/PostResource.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Resources\Json\JsonResource&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;PostResource&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JsonResource&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;toArray&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="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'id'&lt;/span&gt;             &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&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;'title'&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;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'author'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserResource&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;whenLoaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="s1"&gt;'comments_count'&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;whenCounted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'comments'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'created_at'&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;created_at&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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;Two methods worth understanding deeply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;whenLoaded('author')&lt;/code&gt;&lt;/strong&gt; — only includes the relationship data if it was already eager-loaded. Without this, accessing &lt;code&gt;$this-&amp;gt;author&lt;/code&gt; inside &lt;code&gt;toArray()&lt;/code&gt; fires a lazy-load query per resource item (N+1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;whenCounted('comments')&lt;/code&gt;&lt;/strong&gt; — only includes the count if &lt;code&gt;withCount('comments')&lt;/code&gt; was called on the query. Neither method triggers additional queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the controller, eager-load everything the Resource needs:&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;// app/Http/Controllers/Api/V1/PostController.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AnonymousResourceCollection&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'comments'&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;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$posts&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;paginate()&lt;/code&gt; works well for small-to-medium datasets. For tables with millions of rows, switch to &lt;code&gt;cursorPaginate()&lt;/code&gt; — it uses keyset pagination on an indexed column and does not degrade with OFFSET like &lt;code&gt;paginate()&lt;/code&gt; does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Validate Inputs Safely
&lt;/h2&gt;

&lt;p&gt;Always use a FormRequest and pass &lt;code&gt;$request-&amp;gt;validated()&lt;/code&gt; to model methods — never &lt;code&gt;$request-&amp;gt;all()&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;// app/Http/Requests/V1/StorePostRequest.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Contracts\Validation\Validator&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\Foundation\Http\FormRequest&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\Exceptions\HttpResponseException&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;StorePostRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormRequest&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;authorize&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rules&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;'title'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:255'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="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;failedValidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Validator&lt;/span&gt; &lt;span class="nv"&gt;$validator&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;HttpResponseException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'errors'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;422&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 &lt;code&gt;failedValidation()&lt;/code&gt; override ensures API clients receive a consistent JSON envelope instead of Laravel's default 422 response format, which varies depending on the &lt;code&gt;Accept&lt;/code&gt; header and exception handler configuration.&lt;/p&gt;

&lt;p&gt;In the controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;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;StorePostRequest&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;PostResource&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'author'&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;Never define &lt;code&gt;$guarded = []&lt;/code&gt; or call &lt;code&gt;forceFill()&lt;/code&gt; on user-controlled input. Mass assignment attacks are entirely preventable — define &lt;code&gt;$fillable&lt;/code&gt; explicitly on every model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Rate Limiting with Redis
&lt;/h2&gt;

&lt;p&gt;In Laravel 11 and 12, rate limiters are configured in &lt;code&gt;bootstrap/app.php&lt;/code&gt; (the &lt;code&gt;Kernel.php&lt;/code&gt; approach from Laravel 10 is gone).&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;// bootstrap/app.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Cache\RateLimiting\Limit&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="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api'&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;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="k"&gt;return&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;?&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&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;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&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;by&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;ip&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 applies automatically to all &lt;code&gt;api.php&lt;/code&gt; routes via the &lt;code&gt;throttle:api&lt;/code&gt; middleware registered during &lt;code&gt;php artisan install:api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; in production, set &lt;code&gt;CACHE_DRIVER=redis&lt;/code&gt;. Using the &lt;code&gt;file&lt;/code&gt; or &lt;code&gt;database&lt;/code&gt; cache driver for rate limiting creates race conditions under concurrent load and introduces disk I/O bottlenecks. The file driver also does not share state across multiple application servers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Configure CORS Correctly
&lt;/h2&gt;

&lt;p&gt;Laravel handles CORS natively via &lt;code&gt;Illuminate\Http\Middleware\HandleCors&lt;/code&gt;. Edit &lt;code&gt;config/cors.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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'paths'&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;'api/*'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'allowed_methods'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'allowed_origins'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FRONTEND_URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://app.example.com'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="s1"&gt;'allowed_headers'&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;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Authorization'&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="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'supports_credentials'&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="s1"&gt;'max_age'&lt;/span&gt;              &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;86400&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;Do &lt;strong&gt;not&lt;/strong&gt; set &lt;code&gt;allowed_origins&lt;/code&gt; to &lt;code&gt;['*']&lt;/code&gt; with &lt;code&gt;supports_credentials =&amp;gt; true&lt;/code&gt;. Browsers reject credentialed requests to wildcard origins (CORS spec requirement) and this configuration is a security misconfiguration regardless.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Prevent N+1 Queries
&lt;/h2&gt;

&lt;p&gt;Add this to &lt;code&gt;AppServiceProvider::boot()&lt;/code&gt; to throw an exception on lazy-loaded relationships in non-production environments:&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;// app/Providers/AppServiceProvider.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;preventLazyLoading&lt;/span&gt;&lt;span class="p"&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;isProduction&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 catches N+1 issues during development and staging before they reach production. Pair it with &lt;code&gt;withCount()&lt;/code&gt; and &lt;code&gt;with()&lt;/code&gt; calls in every controller method that returns a collection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 9: Write Feature Tests
&lt;/h2&gt;

&lt;p&gt;Test the HTTP contract, not implementation details. Laravel's &lt;code&gt;actingAs()&lt;/code&gt; with the &lt;code&gt;'sanctum'&lt;/code&gt; guard makes authenticated API tests straightforward:&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;// tests/Feature/Api/V1/PostTest.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Foundation\Testing\RefreshDatabase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'authenticated user can create a post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanctum'&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;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Hello World'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Body text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertJsonStructure&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'unauthenticated request returns 401'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'x'&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;assertUnauthorized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'validation rejects missing content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanctum'&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;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'No content'&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;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&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;assertJsonPath&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="kc"&gt;false&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;assertJsonStructure&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'errors'&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;'content'&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;Use &lt;code&gt;RefreshDatabase&lt;/code&gt; to reset state between tests. Test the 422 envelope shape explicitly — your &lt;code&gt;failedValidation()&lt;/code&gt; override is part of the API contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Skipping &lt;code&gt;php artisan install:api&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;routes/api.php&lt;/code&gt; exists in L11/L12 fresh installs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;$request-&amp;gt;all()&lt;/code&gt; in &lt;code&gt;Model::create()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;$request-&amp;gt;validated()&lt;/code&gt; always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;APP_DEBUG=true&lt;/code&gt; in production&lt;/td&gt;
&lt;td&gt;Full stack traces leak in JSON error responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;allowed_origins: ['*']&lt;/code&gt; with credentials&lt;/td&gt;
&lt;td&gt;Browsers reject it; enumerate origins explicitly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File/database cache for rate limiting&lt;/td&gt;
&lt;td&gt;Use Redis; file driver has race conditions under load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;whenLoaded()&lt;/code&gt; in Resources&lt;/td&gt;
&lt;td&gt;Every resource item triggers a lazy-load query (N+1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No versioning from day one&lt;/td&gt;
&lt;td&gt;Retrofitting &lt;code&gt;/api/v2/&lt;/code&gt; after launch is painful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passport 13.0.0–13.7.0 with &lt;code&gt;client_credentials&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;CVE-2026-39976 — upgrade to 13.7.1+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Production Deployment Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;APP_DEBUG=false&lt;/code&gt; and &lt;code&gt;APP_ENV=production&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CACHE_DRIVER=redis&lt;/code&gt;, &lt;code&gt;QUEUE_CONNECTION=redis&lt;/code&gt;, &lt;code&gt;SESSION_DRIVER=redis&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Rotate &lt;code&gt;APP_KEY&lt;/code&gt; before first deploy (invalidates sessions and signed URLs)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;composer audit&lt;/code&gt; to check installed packages against the PHP Security Advisories Database&lt;/li&gt;
&lt;li&gt;Configure Horizon with named queues (&lt;code&gt;critical&lt;/code&gt;, &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;emails&lt;/code&gt;) and &lt;code&gt;--max-jobs&lt;/code&gt; + &lt;code&gt;--max-time&lt;/code&gt; flags on workers to cap memory drift&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;secure&lt;/code&gt; flag to &lt;code&gt;true&lt;/code&gt; on cookies and enforce HTTPS with HSTS headers&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;For a broader look at authentication strategies, pagination patterns, and Octane configuration for high-throughput APIs, see &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-api-guide" rel="noopener noreferrer"&gt;Building Production-Ready APIs with Laravel&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>api</category>
      <category>rest</category>
    </item>
    <item>
      <title>Livewire 4 Patterns for Interactive Laravel Interfaces</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Tue, 25 Aug 2026 10:01:04 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/livewire-4-patterns-for-interactive-laravel-interfaces-1maa</link>
      <guid>https://dev.to/mumbai_web_designer/livewire-4-patterns-for-interactive-laravel-interfaces-1maa</guid>
      <description>&lt;h1&gt;
  
  
  Livewire 4 Patterns for Interactive Laravel Interfaces
&lt;/h1&gt;

&lt;p&gt;Livewire 4.0 shipped on January 15, 2026, and the latest patch as of June 2026 is v4.3.1. If you are building interactive UIs inside Laravel Blade without reaching for a full JavaScript framework, this release changes what is practical — not just what is possible.&lt;/p&gt;

&lt;p&gt;This post focuses on five specific patterns: Islands for isolated re-renders, async actions to unblock parallel requests, the corrected &lt;code&gt;wire:model&lt;/code&gt; modifier chain, SPA-like navigation with &lt;code&gt;wire:navigate&lt;/code&gt;, and CSP-safe configuration. Each section covers the correct usage, the common mistake, and the tradeoff.&lt;/p&gt;

&lt;p&gt;For the broader frontend landscape (Inertia.js, React, Vue, Alpine.js, and how to choose between them), see &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-frontends-livewire-inertia" rel="noopener noreferrer"&gt;Modern Laravel Frontends: Livewire 4, Inertia, React, Vue, and Alpine.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Laravel 11+, PHP 8.2+, Livewire v4.3.1 (&lt;code&gt;composer require livewire/livewire:^4.0&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 1 — Islands: Isolate Expensive Re-Renders
&lt;/h2&gt;

&lt;p&gt;Livewire's default update model re-renders the entire component on every server round-trip. For simple CRUD forms this is fine. For components that mix a frequently-updated region (a live search box) with an expensive region (a paginated data table with complex joins), it is wasteful.&lt;/p&gt;

&lt;p&gt;Livewire 4 introduces the &lt;code&gt;@island&lt;/code&gt; directive. Wrapping a region in &lt;code&gt;@island&lt;/code&gt; tells Livewire to treat that sub-region as an isolated render unit. Actions triggered inside an island only cause that island to re-render — the rest of the component is untouched.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{-- resources/views/livewire/product-dashboard.blade.php --}}
&amp;lt;div&amp;gt;
    {{-- This region re-renders on its own update cycle --}}
    @island('search-results')
        &amp;lt;div&amp;gt;
            &amp;lt;livewire:product-search :query="$query" /&amp;gt;
        &amp;lt;/div&amp;gt;
    @endisland

    {{-- Refresh only the island, not the full component --}}
    &amp;lt;button wire:click.island="refreshSearch"&amp;gt;Refresh results&amp;lt;/button&amp;gt;

    {{-- This expensive region is NOT re-rendered when the island updates --}}
    &amp;lt;div class="mt-8"&amp;gt;
        @include('partials.featured-products', ['products' =&amp;gt; $featuredProducts])
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this replaces:&lt;/strong&gt; Creating a separate child Livewire component just for isolation. With islands you get isolated re-renders without the overhead of a full independent component lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common mistake:&lt;/strong&gt; Leaving expensive query data outside the island boundary — it still runs on every full-component update. Use &lt;code&gt;#[Lazy]&lt;/code&gt; or move the query inside the island's scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff:&lt;/strong&gt; Islands cannot be reused across Blade files. If you duplicate &lt;code&gt;@island&lt;/code&gt; blocks, extract a proper child component instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 2 — Async Actions: Fire-and-Forget Without Blocking
&lt;/h2&gt;

&lt;p&gt;In Livewire 3, every action was synchronous from the user's perspective — clicking one button queued its request, and the next interaction waited until the response came back. Livewire 4 adds an async modifier that decouples the HTTP request from the UI update cycle.&lt;/p&gt;

&lt;p&gt;Two equivalent ways to mark an action as async:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{-- Modifier on the wire:click directive --}}
&amp;lt;button wire:click.async="logActivity"&amp;gt;Track&amp;lt;/button&amp;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="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Livewire&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;Livewire\Component&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;Livewire\Attributes\Async&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;ProductCard&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// PHP attribute approach — keeps the Blade template clean&lt;/span&gt;
    &lt;span class="na"&gt;#[Async]&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;logActivity&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;// Writes to analytics, sends to a queue, etc.&lt;/span&gt;
        &lt;span class="c1"&gt;// Does not block other wire:model.live inputs or clicks&lt;/span&gt;
        &lt;span class="nf"&gt;activity&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'product-viewed'&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;render&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;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'livewire.product-card'&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;When to reach for async:&lt;/strong&gt; Logging, analytics, non-critical side effects, queue dispatching — anything where the user doesn't need the response to keep interacting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When not to use it:&lt;/strong&gt; Any action that modifies state the user immediately sees. Async actions don't guarantee execution order relative to synchronous ones — a race between async &lt;code&gt;logActivity&lt;/code&gt; and synchronous &lt;code&gt;updateCart&lt;/code&gt; can leave component state inconsistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel requests in v4:&lt;/strong&gt; &lt;code&gt;wire:model.live&lt;/code&gt; fields now fire in parallel by default (v3 serialised them) — no change needed beyond upgrading.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 3 — The Corrected wire:model Modifier Chain
&lt;/h2&gt;

&lt;p&gt;This is the Livewire 4 change most likely to silently break existing v3 components after an upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it worked in v3:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{-- v3: .blur meant "send a network request when the input loses focus" --}}
&amp;lt;input wire:model.blur="email"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works in v4:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{-- v4: .blur now controls CLIENT-SIDE sync timing only --}}
{{-- This syncs the JS value on blur, but sends NO network request --}}
&amp;lt;input wire:model.blur="email"&amp;gt;

{{-- v4: Add .live to trigger the actual network request --}}
{{-- Sends a request when the input loses focus --}}
&amp;lt;input wire:model.live.blur="email"&amp;gt;

{{-- Send a request 300ms after the user stops typing --}}
&amp;lt;input wire:model.live.debounce.300ms="email"&amp;gt;

{{-- Lazy — sync client value immediately, send request on blur --}}
&amp;lt;input wire:model.live.lazy="email"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it changed:&lt;/strong&gt; In v4, the modifier chain follows a consistent two-layer model. The first layer is always the network timing (&lt;code&gt;.live&lt;/code&gt;, &lt;code&gt;.lazy&lt;/code&gt;, or nothing for deferred). The second layer is the client-side sync timing (&lt;code&gt;.blur&lt;/code&gt;, &lt;code&gt;.debounce&lt;/code&gt;, &lt;code&gt;.throttle&lt;/code&gt;). The v3 shortcut conflated these two concerns, which caused edge cases when combining modifiers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upgrade checklist:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'wire:model\.blur\|wire:model\.change'&lt;/span&gt; resources/views/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace each hit with &lt;code&gt;wire:model.live.blur&lt;/code&gt; or &lt;code&gt;wire:model.live.change&lt;/code&gt;. Verify the network request fires at the expected time in the browser's Network tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common mistake:&lt;/strong&gt; Finding zero grep results and assuming the app is clean. &lt;code&gt;wire:model&lt;/code&gt; with no modifier now defaults to deferred batch updates at form submission — review every bare &lt;code&gt;wire:model&lt;/code&gt;, not just the ones that used &lt;code&gt;.blur&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 4 — wire:navigate for SPA-Like Page Transitions
&lt;/h2&gt;

&lt;p&gt;Livewire 4 ships a built-in SPA navigation mode. Adding &lt;code&gt;wire:navigate&lt;/code&gt; to anchor tags replaces full-page browser navigation with a Livewire-managed fetch, a DOM swap, and browser history API updates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{-- Standard SPA navigation --}}
&amp;lt;a href="/dashboard" wire:navigate&amp;gt;Dashboard&amp;lt;/a&amp;gt;

{{-- Prefetch the page after 75ms hover --}}
&amp;lt;a href="/products" wire:navigate.hover&amp;gt;Products&amp;lt;/a&amp;gt;

{{-- Inside a nav partial used across all pages --}}
&amp;lt;nav&amp;gt;
    &amp;lt;a href="/" wire:navigate.hover&amp;gt;Home&amp;lt;/a&amp;gt;
    &amp;lt;a href="/services" wire:navigate.hover&amp;gt;Services&amp;lt;/a&amp;gt;
    &amp;lt;a href="/contact" wire:navigate.hover&amp;gt;Contact&amp;lt;/a&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What prefetch does:&lt;/strong&gt; After the user hovers for 75ms, Livewire fetches the target page in the background. If the user then clicks, the page swap is near-instant because the HTML is already in memory. On slower connections — common on mobile networks in India — the perceived performance improvement is significant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alpine.js state across navigations:&lt;/strong&gt; Alpine components are torn down on each &lt;code&gt;wire:navigate&lt;/code&gt; swap. Mark persistent elements with &lt;code&gt;x-persist&lt;/code&gt; (Alpine 3.x) or move global state to a Livewire persistent component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff vs. Inertia.js:&lt;/strong&gt; &lt;code&gt;wire:navigate&lt;/code&gt; gives SPA-like navigation with zero JS framework overhead. If your team is PHP-first and pages are Blade-driven, it covers most SPA UX needs. For rich client-side state (React hooks, Pinia stores), use Inertia instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 5 — CSP Safe Mode
&lt;/h2&gt;

&lt;p&gt;Both Livewire and Alpine.js evaluate JavaScript expressions using &lt;code&gt;new Function()&lt;/code&gt; by default. This violates a &lt;code&gt;Content-Security-Policy&lt;/code&gt; header that disallows &lt;code&gt;unsafe-eval&lt;/code&gt; — a header that provides meaningful XSS protection in production.&lt;/p&gt;

&lt;p&gt;Livewire 4 adds a &lt;code&gt;csp_safe&lt;/code&gt; flag in &lt;code&gt;config/livewire.php&lt;/code&gt;. Setting it to &lt;code&gt;true&lt;/code&gt; switches Livewire to a pre-compiled expression evaluator and — automatically — forces Alpine.js into its own CSP-safe evaluator as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// config/livewire.php&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;'csp_safe'&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="c1"&gt;// ... other config&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your HTTP response headers can then include:&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'; style-src 'self' 'unsafe-inline';
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What breaks when you enable this:&lt;/strong&gt; Alpine directives that use complex JS expressions (&lt;code&gt;$event.detail.productId&lt;/code&gt;, &lt;code&gt;window.myGlobalVar&lt;/code&gt;, ternary chains) will fail — the CSP-safe evaluator supports only a limited syntax subset.&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'x-on\|x-bind\|@click\|:class'&lt;/span&gt; resources/views/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'\$event\.detail|window\.'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace complex expressions with named Alpine methods in &lt;code&gt;x-data&lt;/code&gt;, or move logic into Livewire actions called via &lt;code&gt;wire:click&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; This removes the &lt;code&gt;unsafe-eval&lt;/code&gt; attack surface from the browser sandbox. For apps handling sensitive data or public-facing forms, the refactor cost is worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security: The Livewire 3 CVE You Cannot Ignore
&lt;/h2&gt;

&lt;p&gt;If any of your applications are still on Livewire 3.x below 3.6.4, stop and upgrade before doing anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2025-54068 (CVSS 9.2, CRITICAL):&lt;/strong&gt; Unauthenticated remote code execution via Livewire v3 component property hydration. An attacker who can guess or obtain the Laravel &lt;code&gt;APP_KEY&lt;/code&gt; can forge a signed payload and execute arbitrary code. Patched in Livewire 3.6.4. Livewire 4.x is not affected.&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 your current Livewire version&lt;/span&gt;
composer show livewire/livewire | &lt;span class="nb"&gt;grep &lt;/span&gt;versions

&lt;span class="c"&gt;# Upgrade Livewire 3 to the patched release&lt;/span&gt;
composer require livewire/livewire:^3.6.4

&lt;span class="c"&gt;# Or upgrade to v4 (recommended for greenfield and early-stage projects)&lt;/span&gt;
composer require livewire/livewire:^4.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your &lt;code&gt;APP_KEY&lt;/code&gt; was ever exposed in a repo or config file committed to version control, rotate it immediately — even after patching.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing Your Livewire 4 Components
&lt;/h2&gt;

&lt;p&gt;Livewire 4 replaced &lt;code&gt;Volt::test()&lt;/code&gt; with &lt;code&gt;Livewire::test()&lt;/code&gt;. If you used Volt in v3, update every test 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="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Livewire\Livewire&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Livewire\ProductSearch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'filters products when query is updated'&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;Livewire&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductSearch&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;set&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="s1"&gt;'laptop'&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;assertSee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MacBook Pro'&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;assertDontSee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Office Chair'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logs activity asynchronously without blocking state'&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="c1"&gt;// Async actions resolve before assertions in test context&lt;/span&gt;
    &lt;span class="nc"&gt;Livewire&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductCard&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logActivity'&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;assertDispatched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'activity-logged'&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;Verifying wire:navigate prefetch:&lt;/strong&gt; Use Playwright or Dusk with network throttling. Hover a &lt;code&gt;wire:navigate.hover&lt;/code&gt; link, wait 100ms, then click — navigation should complete in under 100ms because the page was prefetched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verifying islands:&lt;/strong&gt; In the Network tab, trigger an island-scoped action and confirm only island HTML appears in the response diff, not the full component HTML.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations Worth Knowing Before You Commit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time push requires Laravel Echo.&lt;/strong&gt; Neither Livewire polling nor &lt;code&gt;wire:navigate&lt;/code&gt; provides WebSocket-based push. You need Laravel Echo + Pusher or Laravel Reverb for real-time data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex client-side UIs hit the round-trip ceiling.&lt;/strong&gt; Drag-and-drop interfaces, rich text editors, and data visualisation charts are better served by dedicated React or Vue libraries. Livewire's server round-trip model adds latency that pure client-side rendering avoids.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Islands cannot be composed like components.&lt;/strong&gt; An &lt;code&gt;@island&lt;/code&gt; block is inlined in one template. If you need the same isolation logic in three different Blade files, extract a child Livewire component instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSP safe mode is all-or-nothing per application.&lt;/strong&gt; There is no per-component toggle. Enabling it changes Alpine's evaluation mode globally.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>livewire</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Practical PHP Attributes Introduced for Laravel Applications</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Tue, 25 Aug 2026 09:11:10 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/practical-php-attributes-introduced-for-laravel-applications-2ldk</link>
      <guid>https://dev.to/mumbai_web_designer/practical-php-attributes-introduced-for-laravel-applications-2ldk</guid>
      <description>&lt;h1&gt;
  
  
  Practical PHP Attributes Introduced for Laravel Applications
&lt;/h1&gt;

&lt;p&gt;PHP 8.0 introduced native attributes — a structured metadata syntax that replaces docblock annotations. Laravel 13 is the first Laravel release to lean into them at framework scale, shipping attribute support across 15+ framework locations. This article walks through every major attribute available in Laravel 13, explains when to use them, and calls out the traps developers fall into when adopting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.3 or higher (hard minimum for Laravel 13)&lt;/li&gt;
&lt;li&gt;Laravel 13.x (&lt;code&gt;composer require laravel/framework:^13.0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Familiarity with Eloquent models, controllers, middleware, and jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full picture of what shipped in Laravel 13 beyond attributes, see &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-13-features-upgrade-guide" rel="noopener noreferrer"&gt;Laravel 13: Features, Upgrade Guide, and Breaking Changes&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What PHP Attributes Actually Are
&lt;/h2&gt;

&lt;p&gt;A PHP attribute is a structured piece of metadata you attach directly to a class, method, property, or parameter using &lt;code&gt;#[...]&lt;/code&gt; syntax. Before PHP 8.0, developers used docblock annotations like &lt;code&gt;@ORM\Column(type="string")&lt;/code&gt;. Those worked only if a library parsed the docblock at runtime. Native attributes are parsed by PHP itself — no string parsing required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before (docblock annotations — library must parse these at runtime)&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * @Table(name="posts")
 * @Fillable({"title", "body"})
 */&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&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="c1"&gt;// After (native PHP attributes — parsed by the PHP engine)&lt;/span&gt;
&lt;span class="na"&gt;#[Table('posts')]&lt;/span&gt;
&lt;span class="na"&gt;#[Fillable('title', 'body')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference matters: native attributes are faster to resolve, work with static analysis tools like PHPStan out of the box, and are visible in IDE autocompletion without special plugins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Eloquent Model Attributes
&lt;/h2&gt;

&lt;p&gt;The most widely used attributes in Laravel 13 live on Eloquent models. They replace a cluster of static properties that every model used to carry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Fillable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Cast&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Table('posts', primaryKey: 'post_id', incrementing: true, timestamps: true)]&lt;/span&gt;
&lt;span class="na"&gt;#[Fillable('title', 'body', 'user_id', 'published_at')]&lt;/span&gt;
&lt;span class="na"&gt;#[Hidden('deleted_at', 'internal_score')]&lt;/span&gt;
&lt;span class="na"&gt;#[Cast('published_at', 'datetime')]&lt;/span&gt;
&lt;span class="na"&gt;#[Cast('metadata', 'array')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What each attribute replaces:&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;Attribute&lt;/th&gt;
&lt;th&gt;Replaces&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#[Table('posts', primaryKey: 'id')]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$table&lt;/code&gt;, &lt;code&gt;$primaryKey&lt;/code&gt;, &lt;code&gt;$incrementing&lt;/code&gt;, &lt;code&gt;$timestamps&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#[Fillable('title', 'body')]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$fillable&lt;/code&gt; array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#[Hidden('secret')]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$hidden&lt;/code&gt; array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#[Cast('column', 'type')]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$casts&lt;/code&gt; array&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Important: Both Syntaxes Work
&lt;/h3&gt;

&lt;p&gt;Attributes do not replace the property-based syntax. If you declare both &lt;code&gt;$fillable&lt;/code&gt; and &lt;code&gt;#[Fillable(...)]&lt;/code&gt; on the same model, the property wins. Pick one approach per model and stay consistent.&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 will NOT behave as expected — $fillable overrides #[Fillable]&lt;/span&gt;
&lt;span class="na"&gt;#[Fillable('title')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&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="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;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'body'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// body is accessible, but attribute is ignored&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Controller and Routing Attributes
&lt;/h2&gt;

&lt;p&gt;Controllers gain &lt;code&gt;#[Middleware]&lt;/code&gt; and &lt;code&gt;#[Authorize]&lt;/code&gt; attributes that move protection declarations out of the constructor and into the method signature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Routing\Attributes\Controllers\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;Illuminate\Routing\Attributes\Controllers\Authorize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&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;'auth'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;          &lt;span class="c1"&gt;// applies to every method in this controller&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&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;'verified'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;      &lt;span class="c1"&gt;// stacked — both run&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// auth + verified middleware runs here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&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;'subscribed'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;                         &lt;span class="c1"&gt;// method-level addition&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&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;'create'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;  &lt;span class="c1"&gt;// policy check&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// auth + verified + subscribed + policy gate runs here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&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;'admin'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;                              &lt;span class="c1"&gt;// only admin needed here&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;destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// auth + verified + admin middleware runs here&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 &lt;code&gt;#[Authorize]&lt;/code&gt; attribute maps directly to &lt;code&gt;$this-&amp;gt;authorize()&lt;/code&gt; — it accepts the ability name and an optional model or array of models. If the gate check fails, Laravel throws an &lt;code&gt;AuthorizationException&lt;/code&gt; before the method body runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  When Controller Attributes Make Sense
&lt;/h3&gt;

&lt;p&gt;Use them when different methods in the same controller have meaningfully different authorization rules. If every method needs the same middleware, the constructor &lt;code&gt;$this-&amp;gt;middleware()&lt;/code&gt; approach remains cleaner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Still valid — and arguably more visible for uniform middleware&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__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="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth:sanctum'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'throttle:api'&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;
  
  
  Job and Queue Attributes
&lt;/h2&gt;

&lt;p&gt;Jobs in Laravel have always accepted configuration through public properties (&lt;code&gt;$queue&lt;/code&gt;, &lt;code&gt;$connection&lt;/code&gt;, &lt;code&gt;$tries&lt;/code&gt;, &lt;code&gt;$timeout&lt;/code&gt;). Attributes provide the same control at the class definition level.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\Attributes\Queue&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;QueueName&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\Queue\Attributes\Connection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\Attributes\Tries&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\Queue\Attributes\Timeout&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\Queue\Attributes\BackoffStrategy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[QueueName('video-processing')]&lt;/span&gt;
&lt;span class="na"&gt;#[Connection('redis')]&lt;/span&gt;
&lt;span class="na"&gt;#[Tries(3)]&lt;/span&gt;
&lt;span class="na"&gt;#[Timeout(120)]&lt;/span&gt;
&lt;span class="na"&gt;#[BackoffStrategy([10, 30, 60])]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TranscodeVideo&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// video processing logic&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;Note that &lt;code&gt;Queue::route()&lt;/code&gt; (also new in Laravel 13) overrides job-level queue/connection configuration from a service provider. If you use both, &lt;code&gt;Queue::route()&lt;/code&gt; takes precedence. Use attributes when the routing belongs to the job itself; use &lt;code&gt;Queue::route()&lt;/code&gt; when you need centralized routing control across many jobs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Artisan Command Attributes
&lt;/h2&gt;

&lt;p&gt;Artisan commands now accept attributes for their signature and description instead of class properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Console\Attributes\Command&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;CommandAttribute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[CommandAttribute('posts:publish {--dry-run}', description: 'Publish all scheduled posts')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PublishScheduledPosts&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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$dryRun&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;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="c1"&gt;// logic here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is syntactically clean, but the practical benefit over &lt;code&gt;protected $signature&lt;/code&gt; and &lt;code&gt;protected $description&lt;/code&gt; is modest. The command still needs to extend &lt;code&gt;Illuminate\Console\Command&lt;/code&gt; and the &lt;code&gt;handle()&lt;/code&gt; method works identically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Listener and Event Attributes
&lt;/h2&gt;

&lt;p&gt;Event listeners can declare their event binding using an attribute rather than type-hinting the event 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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Events\Attributes\ListensTo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[ListensTo(PostPublished::class)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendPublishedNotification&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;PostPublished&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="c1"&gt;// send notification&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;For listeners auto-discovered by Laravel's event system, this attribute makes the binding explicit and statically analysable without requiring the listener to be registered in &lt;code&gt;EventServiceProvider&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes and Limitations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Mass-converting existing models without a plan
&lt;/h3&gt;

&lt;p&gt;The most common mistake teams make is running a find-and-replace on &lt;code&gt;$fillable&lt;/code&gt; and &lt;code&gt;$hidden&lt;/code&gt; across every model. This creates a large diff with zero runtime benefit and introduces inconsistency if any model gets missed. Adopt attributes incrementally — new models first, existing models during refactors.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Assuming attributes provide runtime validation
&lt;/h3&gt;

&lt;p&gt;PHP attributes are metadata. &lt;code&gt;#[Fillable('title')]&lt;/code&gt; does not throw if you try to mass-assign &lt;code&gt;body&lt;/code&gt; — Laravel reads the attribute at model boot and populates its internal &lt;code&gt;$fillable&lt;/code&gt; array. The validation behaviour is identical to the property approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Stacking conflicting middleware attributes
&lt;/h3&gt;

&lt;p&gt;Attributes stack. If a parent class has &lt;code&gt;#[Middleware('auth')]&lt;/code&gt; and a child class adds &lt;code&gt;#[Middleware('auth')]&lt;/code&gt; again, both run — resulting in the middleware executing twice. Always check the inheritance chain before stacking.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Forgetting import statements
&lt;/h3&gt;

&lt;p&gt;Attributes require a &lt;code&gt;use&lt;/code&gt; statement for each attribute class. Unlike properties, which are just array values, each attribute has a fully qualified class name. A missing import causes a fatal error at class-load time, not at the point of use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Missing this import will throw at model boot, not at query time&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Fillable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Mixing property and attribute syntax in the same model
&lt;/h3&gt;

&lt;p&gt;As noted above, the property-based values take precedence. If you have both on the same model, the attribute is silently ignored. Enable PHPStan or run a project-wide grep after migration to confirm there are no mixed cases:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s1"&gt;'protected \$fillable\|#\[Fillable'&lt;/span&gt; app/Models/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Models and Controllers Using Attributes
&lt;/h2&gt;

&lt;p&gt;Attributes do not change how you test models or controllers — the test API is identical. You can verify attribute resolution indirectly:&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;// Test that fillable is correctly resolved from #[Fillable] attribute&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_post_fillable_fields&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;$post&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;Post&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'body'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'published_at'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getFillable&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Test that middleware protects the route (controller attribute in effect)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_store_requires_auth&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;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Test that unauthorized user cannot create a post (Authorize attribute)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_store_requires_create_permission&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;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// user without create permission&lt;/span&gt;
    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;actingAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/posts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&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;
  
  
  When to Use Attributes vs. Properties
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New model in a greenfield project&lt;/td&gt;
&lt;td&gt;Use attributes — consistent from the start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing model mid-project&lt;/td&gt;
&lt;td&gt;Keep properties until a natural refactor point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller with varied per-method authorization&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;#[Authorize]&lt;/code&gt; and &lt;code&gt;#[Middleware]&lt;/code&gt; — it's more readable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller with uniform middleware for all methods&lt;/td&gt;
&lt;td&gt;Keep constructor &lt;code&gt;$this-&amp;gt;middleware()&lt;/code&gt; — less noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job configuration owned by the job itself&lt;/td&gt;
&lt;td&gt;Use job attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job routing owned by ops / infrastructure&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;Queue::route()&lt;/code&gt; in a service provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team unfamiliar with PHP attribute syntax&lt;/td&gt;
&lt;td&gt;Defer adoption — the old syntax works indefinitely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Static Analysis and Tooling Support
&lt;/h2&gt;

&lt;p&gt;Native PHP attributes integrate cleanly with the PHP ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHPStan / Psalm&lt;/strong&gt;: Both understand &lt;code&gt;#[...]&lt;/code&gt; attribute syntax natively. No special stubs needed for the attributes themselves, though you may need Laravel-specific stubs for the IDE to understand what each attribute does at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PhpStorm / VS Code with Intelephense&lt;/strong&gt;: Full autocompletion on attribute class names and constructor parameters once the &lt;code&gt;laravel/framework&lt;/code&gt; source is indexed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel Telescope / Debugbar&lt;/strong&gt;: Attribute-configured middleware and authorization run identically to property-based configuration — the debug bar output looks the same.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;PHP Attributes in Laravel 13 are an additive, optional improvement to developer ergonomics. They collocate configuration with the class it belongs to, make metadata visible to static analysis tools, and reduce the number of class properties a model or controller carries. They do not change runtime behaviour — every attribute maps to an existing framework mechanism.&lt;/p&gt;

&lt;p&gt;The practical rule: adopt them in new code, migrate existing code only during planned refactors, and never mix both syntaxes in the same class.&lt;/p&gt;




&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-13-features-upgrade-guide" rel="noopener noreferrer"&gt;Read the full guide&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Composer Security Practices for Laravel Projects</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Tue, 25 Aug 2026 06:13:21 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/composer-security-practices-for-laravel-projects-123g</link>
      <guid>https://dev.to/mumbai_web_designer/composer-security-practices-for-laravel-projects-123g</guid>
      <description>&lt;h1&gt;
  
  
  Composer Security Practices for Laravel Projects
&lt;/h1&gt;

&lt;p&gt;If you are running &lt;code&gt;composer update&lt;/code&gt; in CI and calling it a security practice, you are doing the opposite of what is safe. This article walks through concrete, version-specific Composer hardening steps for Laravel projects — from lockfile discipline to CI egress controls — using lessons from the May 2026 Laravel-Lang supply chain attack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Composer 2.4 or later, Laravel 10 / 11 / 12, a CI pipeline (GitHub Actions examples used throughout).&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Composer Is a High-Value Attack Surface
&lt;/h2&gt;

&lt;p&gt;The average Laravel application pulls in 80–120 Composer packages. Each package maintainer's GitHub account is a potential entry point for an attacker. The May 2026 Laravel-Lang incident proved this: a single GitHub organization compromise let an attacker rewrite every git tag across four packages — &lt;code&gt;laravel-lang/lang&lt;/code&gt;, &lt;code&gt;laravel-lang/attributes&lt;/code&gt;, &lt;code&gt;laravel-lang/http-statuses&lt;/code&gt;, and &lt;code&gt;laravel-lang/actions&lt;/code&gt; — within a 90-minute window. Over 5,500 downstream repositories received a backdoored &lt;code&gt;helpers.php&lt;/code&gt; within six hours.&lt;/p&gt;

&lt;p&gt;That file was wired into Composer's &lt;code&gt;autoload.files&lt;/code&gt; directive, which means it executed on &lt;strong&gt;every PHP request&lt;/strong&gt; once installed. It silently exfiltrated &lt;code&gt;.env&lt;/code&gt; files, AWS keys, GitHub tokens, Stripe secrets, SSH keys, and more to the attacker-controlled domain &lt;code&gt;flipboxstudio.info&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The mechanism that made this possible — and that most teams overlook — is that &lt;strong&gt;git tags are mutable&lt;/strong&gt;. When you pin &lt;code&gt;"laravel-lang/lang": "^2.1"&lt;/code&gt; in &lt;code&gt;composer.json&lt;/code&gt;, Composer resolves the tag at install time. If that tag is silently rewritten to point at a malicious commit, &lt;code&gt;composer update&lt;/code&gt; will pull the new payload. The only immutable anchor is the SHA-256 hash recorded in &lt;code&gt;composer.lock&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rule 1: Commit &lt;code&gt;composer.lock&lt;/code&gt; and Never Run &lt;code&gt;composer update&lt;/code&gt; in CI
&lt;/h2&gt;

&lt;p&gt;This is the single highest-impact change you can make.&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;# Safe — uses exact SHAs from composer.lock&lt;/span&gt;
composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--optimize-autoloader&lt;/span&gt;

&lt;span class="c"&gt;# Dangerous in CI or production — upgrades deps and rewrites composer.lock&lt;/span&gt;
composer update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;composer install&lt;/code&gt; respects the lockfile exactly. &lt;code&gt;composer update&lt;/code&gt; re-resolves every constraint, which can pull in any newly tagged (or re-tagged) version.&lt;/p&gt;

&lt;p&gt;In your GitHub Actions workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install PHP dependencies&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer install --no-dev --optimize-autoloader --no-interaction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; run &lt;code&gt;composer update&lt;/code&gt; as part of any automated pipeline. Reserve it for a dedicated branch where a human reviews the diff before merging. If &lt;code&gt;composer.lock&lt;/code&gt; is not committed to your repository, fix that before anything else:&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 composer.lock
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: commit composer.lock for reproducible builds"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Rule 2: Run &lt;code&gt;composer audit&lt;/code&gt; on Every Build
&lt;/h2&gt;

&lt;p&gt;Composer 2.4 introduced the &lt;code&gt;audit&lt;/code&gt; command, which reads &lt;code&gt;composer.lock&lt;/code&gt; and checks every installed package against the &lt;a href="https://github.com/FriendsOfPHP/security-advisories" rel="noopener noreferrer"&gt;PHP Security Advisories Database&lt;/a&gt;. It takes a few seconds and surfaces real CVEs.&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;# Human-readable output&lt;/span&gt;
composer audit

&lt;span class="c"&gt;# Machine-parseable output for CI artifact storage&lt;/span&gt;
composer audit &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it as a required step in CI, placed after &lt;code&gt;composer install&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Security audit&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer audit --format=json &amp;gt; composer-audit.json&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload audit results&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer-audit&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer-audit.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not suppress audit output with &lt;code&gt;COMPOSER_NO_AUDIT=1&lt;/code&gt;. Every advisory that surfaces represents a real CVE that should be triaged, not silenced.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rule 3: Understand Composer 2.9 Security Blocking
&lt;/h2&gt;

&lt;p&gt;Composer 2.9 (released 7 November 2025) added automatic security blocking via two new config keys:&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;"config"&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;"audit"&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;"block-insecure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"block-abandoned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ignore-abandoned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"some/abandoned-package"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="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;&lt;code&gt;block-insecure&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;, meaning &lt;code&gt;composer update&lt;/code&gt; will fail if any installed package has a known security advisory. This broke CI builds for teams with unresolved advisories in existing dependencies — builds that previously succeeded now fail.&lt;/p&gt;

&lt;p&gt;The correct response is to &lt;strong&gt;resolve the advisory&lt;/strong&gt;, not to disable the feature. If you have a legitimate reason to suppress a specific advisory (for example, a vulnerability that does not affect your usage pattern), audit-ignore it explicitly rather than turning off the entire mechanism:&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;# Acknowledge a specific advisory without disabling the feature&lt;/span&gt;
composer audit &lt;span class="nt"&gt;--ignore-severity&lt;/span&gt; low
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: A bug in early Composer 2.9 releases (GitHub issue #12607) caused &lt;code&gt;COMPOSER_NO_AUDIT=1&lt;/code&gt; and &lt;code&gt;--no-audit&lt;/code&gt; to be ignored by the new blocking logic. Verify your Composer version with &lt;code&gt;composer --version&lt;/code&gt; if you rely on those flags.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rule 4: Add Enlightn as a Complementary Scanner
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;composer audit&lt;/code&gt; covers the PHP Security Advisories Database. The &lt;a href="https://github.com/enlightn/laravel-security-checker" rel="noopener noreferrer"&gt;Enlightn Laravel Security Checker&lt;/a&gt; adds Laravel-specific checks and integrates with Artisan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; enlightn/laravel-security-checker
php artisan security:check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enlightn can also be scheduled to email your team when new vulnerabilities appear in your installed packages — useful for production monitoring between deployments:&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;// app/Console/Kernel.php&lt;/span&gt;
&lt;span class="nv"&gt;$schedule&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'security:check'&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;weekly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tradeoff: &lt;code&gt;composer audit&lt;/code&gt; is zero-dependency and ships with every Composer 2.4+ installation. Enlightn requires an additional dev dependency but gives you scheduler integration and a broader Laravel-specific rule set. Most teams benefit from running both.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rule 5: Enforce CI Egress Controls
&lt;/h2&gt;

&lt;p&gt;The Laravel-Lang attack exfiltrated secrets by making outbound HTTP connections from build machines. GitHub Actions, by default, allows any process running in a workflow to make arbitrary outbound connections — including a backdoored Composer package.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/step-security/harden-runner" rel="noopener noreferrer"&gt;StepSecurity Harden-Runner&lt;/a&gt; adds network-policy enforcement to GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;step-security/harden-runner@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;egress-policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;block&lt;/span&gt;
          &lt;span class="na"&gt;allowed-endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;packagist.org:443&lt;/span&gt;
            &lt;span class="s"&gt;repo.packagist.org:443&lt;/span&gt;
            &lt;span class="s"&gt;github.com:443&lt;/span&gt;
            &lt;span class="s"&gt;api.github.com:443&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

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

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Security audit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer audit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;egress-policy: block&lt;/code&gt;, any process in the workflow that tries to connect to &lt;code&gt;flipboxstudio.info&lt;/code&gt; (or any domain not in your allowlist) will be blocked and logged. This is the exact mechanism that would have contained the May 2026 Laravel-Lang attack in CI environments with this control in place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff:&lt;/strong&gt; You need to allowlist every outbound endpoint your build touches — Packagist, GitHub, your CDN, any API called during tests. Misconfigurations break legitimate builds. The allowlist is typically 5–10 entries and stable once configured.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rule 6: Check for the Laravel-Lang Compromise Indicator
&lt;/h2&gt;

&lt;p&gt;If your project uses any of the four affected packages, verify your build history:&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 whether affected packages are in your lockfile&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'"laravel-lang/(lang|attributes|http-statuses|actions)"'&lt;/span&gt; composer.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Indicator of compromise: outbound connections to &lt;code&gt;flipboxstudio.info&lt;/code&gt; in network logs from your build machines or containers between 22–23 May 2026 UTC. If found, rotate all secrets accessible from your CI environment immediately — GitHub tokens, AWS credentials, Stripe keys, &lt;code&gt;.env&lt;/code&gt; values.&lt;/p&gt;

&lt;p&gt;Packagist.org now enforces &lt;strong&gt;stable version immutability&lt;/strong&gt; for published releases, preventing re-tagging. This applies only to packages hosted on Packagist. Private forks or packages installed via &lt;code&gt;repositories&lt;/code&gt; entries in &lt;code&gt;composer.json&lt;/code&gt; do not benefit from this protection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Patch Status for Active CVEs
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;composer audit&lt;/code&gt; to surface these automatically, but know the target versions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;th&gt;Fixed In&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;laravel/framework&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CVE-2025-27515 (wildcard file validation bypass)&lt;/td&gt;
&lt;td&gt;10.48.29 / 11.44.1 / 12.1.1&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;livewire/livewire&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CVE-2025-54068 (unauthenticated RCE, CISA KEV)&lt;/td&gt;
&lt;td&gt;3.6.4&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;laravel/passport&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CVE-2026-39976 (authentication bypass, Passport 13.x)&lt;/td&gt;
&lt;td&gt;13.7.1&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;plank/laravel-mediable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CVE-2026-4809 (arbitrary file upload / RCE)&lt;/td&gt;
&lt;td&gt;&amp;gt; 6.4.0&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Livewire CVE (CVE-2025-54068) deserves emphasis: it bypasses the APP_KEY-signed checksum mechanism used for component state hydration. Attackers do not need your application key to exploit it. CISA confirmed active exploitation. If you are on any Livewire v3 release below 3.6.4, update immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer update livewire/livewire
composer audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After upgrading Livewire, test any components that pass complex objects through properties — the patch tightened how untrusted input is handled during hydration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification Checklist
&lt;/h2&gt;

&lt;p&gt;Before considering your Composer security posture production-ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;composer.lock&lt;/code&gt; is committed and not in &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] CI runs &lt;code&gt;composer install&lt;/code&gt;, not &lt;code&gt;composer update&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;composer audit&lt;/code&gt; runs as a required CI step and fails the build on findings&lt;/li&gt;
&lt;li&gt;[ ] Composer version is 2.9+ (&lt;code&gt;composer --version&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;audit.block-insecure&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; in &lt;code&gt;composer.json&lt;/code&gt; config (or Composer 2.9 default)&lt;/li&gt;
&lt;li&gt;[ ] Livewire is on 3.6.4 or later&lt;/li&gt;
&lt;li&gt;[ ] Laravel Passport is on 13.7.1 or later (if using Passport 13.x)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;laravel/framework&lt;/code&gt; is on 10.48.29 / 11.44.1 / 12.1.1 or later&lt;/li&gt;
&lt;li&gt;[ ] CI egress is restricted to known-good endpoints&lt;/li&gt;
&lt;li&gt;[ ] Network logs checked for &lt;code&gt;flipboxstudio.info&lt;/code&gt; connections (if using laravel-lang packages)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;Pinning to a version constraint instead of committing the lockfile.&lt;/strong&gt; Writing &lt;code&gt;"laravel-lang/lang": "^2.1"&lt;/code&gt; in &lt;code&gt;composer.json&lt;/code&gt; is not a pin — it is a range that resolves at install time. The lockfile SHA is the real pin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running &lt;code&gt;composer update&lt;/code&gt; on a schedule in CI.&lt;/strong&gt; Automated upgrades are appealing but introduce the risk of pulling in newly compromised package versions without human review of the diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting &lt;code&gt;COMPOSER_NO_AUDIT=1&lt;/code&gt; globally.&lt;/strong&gt; This silences real CVE warnings. Each advisory should be triaged and either patched or explicitly acknowledged with a documented reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming &lt;code&gt;autoload.files&lt;/code&gt; packages are low-risk.&lt;/strong&gt; Any package listed under &lt;code&gt;autoload.files&lt;/code&gt; executes on every PHP request. The Laravel-Lang backdoor used exactly this mechanism.&lt;/p&gt;




&lt;p&gt;For a broader look at supply chain attack anatomy, CVE timelines, and static analysis tooling for Laravel applications, see the &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-security-guide" rel="noopener noreferrer"&gt;Laravel Security Guide: Supply-Chain Risks, Composer, and Application Scanning&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-security-guide" rel="noopener noreferrer"&gt;Read the full guide&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Scanning Laravel Applications with Ward and Other Security Tools</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Tue, 25 Aug 2026 04:18:39 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/scanning-laravel-applications-with-ward-and-other-security-tools-lnj</link>
      <guid>https://dev.to/mumbai_web_designer/scanning-laravel-applications-with-ward-and-other-security-tools-lnj</guid>
      <description>&lt;h1&gt;
  
  
  Scanning Laravel Applications with Ward and Other Security Tools
&lt;/h1&gt;

&lt;p&gt;If you maintain a Laravel application in production, you already know that &lt;code&gt;composer audit&lt;/code&gt; exists. What you may not have set up yet is a layered scanning pipeline that catches what &lt;code&gt;composer audit&lt;/code&gt; misses — hardcoded secrets, insecure Blade output, weak crypto configuration, and runtime file-upload bypasses. This article walks through assembling exactly that pipeline using Ward, &lt;code&gt;composer audit&lt;/code&gt;, and StackShield, with concrete commands and a realistic CI integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.1+ and Composer 2.4+ (Composer 2.9+ strongly recommended)&lt;/li&gt;
&lt;li&gt;Laravel 10, 11, or 12 (examples tested against 12.x)&lt;/li&gt;
&lt;li&gt;Go 1.21+ if building Ward from source (or use the pre-built binary)&lt;/li&gt;
&lt;li&gt;A GitHub Actions or similar CI environment&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why &lt;code&gt;composer audit&lt;/code&gt; Alone Is Not Enough
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;composer audit&lt;/code&gt; (available since Composer 2.4) does one thing well: it reads your &lt;code&gt;composer.lock&lt;/code&gt; and cross-references every installed package against the &lt;a href="https://github.com/FriendsOfPHP/security-advisories" rel="noopener noreferrer"&gt;PHP Security Advisories Database&lt;/a&gt;. Run it in any project:&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;# Standard audit — human-readable output&lt;/span&gt;
composer audit

&lt;span class="c"&gt;# Machine-parseable JSON for CI artifact storage&lt;/span&gt;
composer audit &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Composer 2.9 (released November 2025) made auditing more aggressive by defaulting &lt;code&gt;audit.block-insecure&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. This means &lt;code&gt;composer update&lt;/code&gt; will fail outright if any package has an unresolved advisory. If your builds started breaking after upgrading Composer, this is why:&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;"config"&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;"audit"&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;"block-insecure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"block-abandoned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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="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 limitation is scope. &lt;code&gt;composer audit&lt;/code&gt; only knows about CVEs that have been published to the advisories database. During the May 2026 Laravel-Lang supply-chain attack, roughly six hours elapsed between initial tag-rewrite and any public advisory. In that window, &lt;code&gt;composer audit&lt;/code&gt; returned clean results on a poisoned lockfile. A defence-in-depth strategy needs additional layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ward: Static Analysis Purpose-Built for Laravel
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Eljakani/ward" rel="noopener noreferrer"&gt;Ward&lt;/a&gt; is a Go binary that understands Laravel project structure — routes, models, controllers, middleware, Blade templates, config files, &lt;code&gt;.env&lt;/code&gt;, and Composer dependencies. It runs 42+ built-in rules grouped into categories: secrets, injection, XSS, debug, crypto, config, and auth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS (Homebrew tap)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;eljakani/tap/ward

&lt;span class="c"&gt;# Linux — download the latest release binary&lt;/span&gt;
curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://github.com/Eljakani/ward/releases/latest/download/ward-linux-amd64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/local/bin/ward &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/ward

&lt;span class="c"&gt;# Verify installation&lt;/span&gt;
ward &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic scan
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scan a Laravel project rooted at the current directory&lt;/span&gt;
ward scan &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Output JSON for programmatic processing&lt;/span&gt;
ward scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ward-results.json

&lt;span class="c"&gt;# Output SARIF for GitHub Code Scanning integration&lt;/span&gt;
ward scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; sarif &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ward-results.sarif

&lt;span class="c"&gt;# Output Markdown for PR comments or reports&lt;/span&gt;
ward scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; markdown &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ward-results.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ward also queries &lt;a href="https://osv.dev" rel="noopener noreferrer"&gt;OSV.dev&lt;/a&gt; in real time, covering the broader open-source vulnerability ecosystem beyond the PHP Security Advisories Database — giving it wider CVE coverage than &lt;code&gt;composer audit&lt;/code&gt; alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Ward Catches That &lt;code&gt;composer audit&lt;/code&gt; Misses
&lt;/h3&gt;

&lt;p&gt;Consider these two common Laravel anti-patterns:&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="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="nc"&gt;Unsafe&lt;/span&gt;&lt;span class="o"&gt;:&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="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;skips&lt;/span&gt; &lt;span class="nc"&gt;Laravel&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;escaping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabling&lt;/span&gt; &lt;span class="no"&gt;XSS&lt;/span&gt; &lt;span class="o"&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="n"&gt;h1&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;!!&lt;/span&gt; &lt;span class="nv"&gt;$userInput&lt;/span&gt; &lt;span class="o"&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="n"&gt;h1&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;--&lt;/span&gt; &lt;span class="nc"&gt;Safe&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="no"&gt;HTML&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;encodes&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&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="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$userInput&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or a raw SQL query that reintroduces injection risk:&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;// Dangerous: direct string interpolation bypasses prepared statements&lt;/span&gt;
&lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM users WHERE email = '&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$email&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="c1"&gt;// Safe: Query Builder uses PDO bindings&lt;/span&gt;
&lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&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;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ward's static analysis flags both patterns. &lt;code&gt;composer audit&lt;/code&gt; is silent on both because they are not CVEs in installed packages — they are code-level vulnerabilities in your own application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ward Findings to Prioritise
&lt;/h3&gt;

&lt;p&gt;After running a scan, focus on the high-severity findings first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Secrets in code&lt;/strong&gt; — hardcoded API keys, tokens, or credentials that should be in &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unescaped Blade output&lt;/strong&gt; — &lt;code&gt;{!! !!}&lt;/code&gt; applied to request or user-controlled data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw SQL with interpolation&lt;/strong&gt; — bypassing the ORM's prepared statements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug mode leaks&lt;/strong&gt; — &lt;code&gt;APP_DEBUG=true&lt;/code&gt; references in non-test config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak encryption&lt;/strong&gt; — use of &lt;code&gt;md5()&lt;/code&gt; or &lt;code&gt;sha1()&lt;/code&gt; for password hashing&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  StackShield: External Black-Box Scanning
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://stackshield.io" rel="noopener noreferrer"&gt;StackShield&lt;/a&gt; takes the opposite approach to Ward. It is a zero-installation external scanner — you supply a URL, it probes your live deployment the way an attacker would, running 30+ checks without needing Composer or server access.&lt;/p&gt;

&lt;p&gt;Use StackShield when you want a quick external audit of a staging environment, confirmation that &lt;code&gt;.env&lt;/code&gt; is not publicly accessible, debug routes are disabled in production, or to check security headers (CSP, HSTS, X-Frame-Options).&lt;/p&gt;

&lt;p&gt;The tradeoff: StackShield cannot read your source code and requires a live, publicly reachable deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ward vs StackShield — when to use each:&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;Concern&lt;/th&gt;
&lt;th&gt;Ward&lt;/th&gt;
&lt;th&gt;StackShield&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source-level XSS patterns&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded secrets in code&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exposed &lt;code&gt;.env&lt;/code&gt; file&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing security headers&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requires live deployment&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requires source code access&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries OSV.dev for CVEs&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run Ward in CI on every push; run StackShield periodically against a live staging environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enlightn as a Complement
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/enlightn/laravel-security-checker" rel="noopener noreferrer"&gt;Enlightn&lt;/a&gt; integrates directly with Laravel's Artisan scheduler and can email your team when new advisories appear against your installed packages:&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;# Install as a dev dependency&lt;/span&gt;
composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; enlightn/laravel-security-checker

&lt;span class="c"&gt;# Run the security check via Artisan&lt;/span&gt;
php artisan security:check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule it in &lt;code&gt;app/Console/Kernel.php&lt;/code&gt; (Laravel 10/11) or &lt;code&gt;routes/console.php&lt;/code&gt; (Laravel 12):&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;// routes/console.php (Laravel 12)&lt;/span&gt;
&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'security:check'&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;daily&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enlightn covers the same PHP Security Advisories Database as &lt;code&gt;composer audit&lt;/code&gt; but adds scheduling, email notifications, and Artisan integration — useful for teams that do not monitor CI dashboards daily.&lt;/p&gt;




&lt;h2&gt;
  
  
  Critical CVEs Your Scanner Should Be Detecting
&lt;/h2&gt;

&lt;p&gt;Make sure your current toolchain flags the following. If it does not, the scanner's database is out of date:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2025-27515&lt;/strong&gt; — File validation bypass in &lt;code&gt;laravel/framework&lt;/code&gt;. Wildcard validation rules like &lt;code&gt;files.*&lt;/code&gt; could be circumvented. Patched in 10.48.29, 11.44.1, and 12.1.1. If you are below these versions, &lt;code&gt;composer audit&lt;/code&gt; should flag this immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2025-54068&lt;/strong&gt; — Unauthenticated RCE in Livewire v3 through v3.6.3. This one bypasses the APP_KEY-signed checksum mechanism entirely — the attacker does not need your application key. It is on CISA's Known Exploited Vulnerabilities catalog. Update to Livewire v3.6.4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2026-39976&lt;/strong&gt; — Authentication bypass in Laravel Passport 13.0.0–13.7.0. The &lt;code&gt;TokenGuard&lt;/code&gt; does not verify whether a JWT &lt;code&gt;sub&lt;/code&gt; claim belongs to a user or a client, allowing machine tokens to impersonate real users when IDs collide. Patched in Passport 13.7.1.&lt;/p&gt;

&lt;p&gt;Verify your current versions:&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 installed versions of affected packages&lt;/span&gt;
composer show laravel/framework | &lt;span class="nb"&gt;grep &lt;/span&gt;versions
composer show livewire/livewire | &lt;span class="nb"&gt;grep &lt;/span&gt;versions
composer show laravel/passport | &lt;span class="nb"&gt;grep &lt;/span&gt;versions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Integrating Ward into GitHub Actions
&lt;/h2&gt;

&lt;p&gt;A complete CI job that runs &lt;code&gt;composer audit&lt;/code&gt; and Ward in sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Security Scan&lt;/span&gt;

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

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;step-security/harden-runner@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;egress-policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;block&lt;/span&gt;
          &lt;span class="na"&gt;allowed-endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;packagist.org:443&lt;/span&gt;
            &lt;span class="s"&gt;repo.packagist.org:443&lt;/span&gt;
            &lt;span class="s"&gt;github.com:443&lt;/span&gt;
            &lt;span class="s"&gt;osv.dev:443&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install PHP dependencies (lockfile only)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer install --no-dev --optimize-autoloader&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Composer audit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer audit --format=json | tee composer-audit.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Ward&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -L https://github.com/Eljakani/ward/releases/latest/download/ward-linux-amd64 \&lt;/span&gt;
            &lt;span class="s"&gt;-o /usr/local/bin/ward &amp;amp;&amp;amp; chmod +x /usr/local/bin/ward&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Ward scan&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ward scan . --output sarif &amp;gt; ward-results.sarif&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload SARIF to GitHub Code Scanning&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ward-results.sarif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;harden-runner&lt;/code&gt; step. This egress-policy enforcement would have contained the May 2026 Laravel-Lang supply-chain attack: blocking outbound connections to &lt;code&gt;flipboxstudio.info&lt;/code&gt; would have silenced the credential exfiltration payload injected via &lt;code&gt;autoload.files&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Not committing &lt;code&gt;composer.lock&lt;/code&gt;.&lt;/strong&gt; Without a committed lockfile, &lt;code&gt;composer install&lt;/code&gt; resolves dependencies fresh each time, making your builds non-reproducible and vulnerable to tag-rewrite attacks. Always commit &lt;code&gt;composer.lock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running &lt;code&gt;composer update&lt;/code&gt; in CI.&lt;/strong&gt; Use &lt;code&gt;composer install&lt;/code&gt; in automated pipelines — it respects the lockfile. Reserve &lt;code&gt;composer update&lt;/code&gt; for deliberate, reviewed dependency bumps in a local or staging environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suppressing audit warnings.&lt;/strong&gt; Setting &lt;code&gt;COMPOSER_NO_AUDIT=1&lt;/code&gt; to silence failing builds defeats the security feature. Triage each advisory; do not silence them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relying on Ward for runtime checks.&lt;/strong&gt; Ward is a static analyser. It will not catch a runtime misconfiguration that only manifests under specific request conditions. Pair it with runtime anomaly detection or penetration testing for complete coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating Ward findings as a one-time task.&lt;/strong&gt; New rules are added as new vulnerability patterns emerge. Re-run Ward on a schedule, not just when onboarding it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing and Verification
&lt;/h2&gt;

&lt;p&gt;After running your scanning pipeline, verify the most critical outputs:&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;# Confirm composer audit exits non-zero when advisories exist&lt;/span&gt;
composer audit&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Exit code: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Parse Ward JSON output to count high-severity findings&lt;/span&gt;
ward scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json | jq &lt;span class="s1"&gt;'[.findings[] | select(.severity == "high")] | length'&lt;/span&gt;

&lt;span class="c"&gt;# Check your lockfile for the May 2026 Laravel-Lang affected packages&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'"laravel-lang/(lang|attributes|http-statuses|actions)"'&lt;/span&gt; composer.lock

&lt;span class="c"&gt;# Verify network egress from your CI runner does not reach the known IOC domain&lt;/span&gt;
&lt;span class="c"&gt;# (check build logs for any connection to flipboxstudio.info)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a broader view of supply-chain risk patterns and how Composer's lockfile semantics interact with tag-rewrite attacks, see the &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-security-guide" rel="noopener noreferrer"&gt;Laravel Security Guide: Supply-Chain Risks, Composer, and Application Scanning&lt;/a&gt;.&lt;/p&gt;




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

&lt;p&gt;No single tool covers the full attack surface of a Laravel application. &lt;code&gt;composer audit&lt;/code&gt; is fast and built-in — run it on every CI push. Ward brings static analysis that understands Laravel's structure and queries a broader vulnerability database. StackShield gives you an external attacker's perspective on a live deployment. Enlightn ties advisory checking into Laravel's scheduler for ongoing team notifications. Stack them, automate them, and do not suppress their warnings.&lt;/p&gt;

&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-security-guide" rel="noopener noreferrer"&gt;Read the full guide&lt;/a&gt;  &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Lessons Laravel Developers Should Learn from the laravel-lang Attack</title>
      <dc:creator>Sumeet Shroff</dc:creator>
      <pubDate>Sat, 22 Aug 2026 10:01:02 +0000</pubDate>
      <link>https://dev.to/mumbai_web_designer/lessons-laravel-developers-should-learn-from-the-laravel-lang-attack-49i4</link>
      <guid>https://dev.to/mumbai_web_designer/lessons-laravel-developers-should-learn-from-the-laravel-lang-attack-49i4</guid>
      <description>&lt;h1&gt;
  
  
  Lessons Laravel Developers Should Learn from the laravel-lang Attack
&lt;/h1&gt;

&lt;p&gt;On 22–23 May 2026, four packages in the &lt;code&gt;laravel-lang&lt;/code&gt; organization — &lt;code&gt;laravel-lang/lang&lt;/code&gt;, &lt;code&gt;laravel-lang/attributes&lt;/code&gt;, &lt;code&gt;laravel-lang/http-statuses&lt;/code&gt;, and &lt;code&gt;laravel-lang/actions&lt;/code&gt; — were silently backdoored through a single account compromise. Every existing git tag across all four packages was rewritten within a 90-minute window. Within six hours, 5,561+ downstream repositories had received the poisoned code.&lt;/p&gt;

&lt;p&gt;The injected payload was a &lt;code&gt;helpers.php&lt;/code&gt; file wired into &lt;code&gt;autoload.files&lt;/code&gt; in &lt;code&gt;composer.json&lt;/code&gt;. Because &lt;code&gt;autoload.files&lt;/code&gt; executes code on every PHP request immediately after installation, the backdoor ran without any action from application code. It exfiltrated AWS keys, GitHub tokens, Stripe secrets, SSH keys, &lt;code&gt;.env&lt;/code&gt; files, JWTs, Kubernetes secrets, and crypto recovery phrases to &lt;code&gt;flipboxstudio.info&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a technical walkthrough of the exact failure modes the attack exploited, and the concrete steps you can take today to prevent the same class of attack against your Laravel projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Composer 2.4+ for &lt;code&gt;composer audit&lt;/code&gt;; 2.9+ for &lt;code&gt;audit.block-insecure&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Laravel 10, 11, or 12&lt;/li&gt;
&lt;li&gt;CI pipeline access (GitHub Actions or equivalent)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Tag Pinning Failed
&lt;/h2&gt;

&lt;p&gt;Most developers assume that pinning a package to a specific version (&lt;code&gt;"laravel-lang/lang": "^6.3"&lt;/code&gt;) means Composer will always install the same code. That assumption is wrong when it comes to git tags.&lt;/p&gt;

&lt;p&gt;Composer resolves a version constraint to a git tag. It then fetches the commit that the tag &lt;em&gt;currently points at&lt;/em&gt;. If a maintainer (or attacker) rewrites the tag to point at a different commit, Composer will pull the new commit — there is no version mismatch, no checksum failure, no warning.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;laravel-lang&lt;/code&gt; attack exploited this precisely. The attackers rewrote every stable tag, so any project running &lt;code&gt;composer update&lt;/code&gt; or even a fresh &lt;code&gt;composer install&lt;/code&gt; with an outdated &lt;code&gt;composer.lock&lt;/code&gt; received the backdoored code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The only tamper-evident anchor Composer has is &lt;code&gt;composer.lock&lt;/code&gt;.&lt;/strong&gt; The lockfile stores the resolved commit SHA alongside the dist hash. If you commit your lockfile and deploy with &lt;code&gt;composer install&lt;/code&gt; (not &lt;code&gt;composer update&lt;/code&gt;), Composer will refuse to install a package whose content hash no longer matches.&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;# Safe — installs exactly what composer.lock specifies, verifies hashes&lt;/span&gt;
composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--optimize-autoloader&lt;/span&gt;

&lt;span class="c"&gt;# Dangerous in CI/production — resolves constraints fresh, rewrites composer.lock&lt;/span&gt;
&lt;span class="c"&gt;# Do not use in automated pipelines&lt;/span&gt;
composer update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the single most impactful change you can make: &lt;strong&gt;never run &lt;code&gt;composer update&lt;/code&gt; in CI or production&lt;/strong&gt;. Run it locally, review the diff in &lt;code&gt;composer.lock&lt;/code&gt;, and commit the result as a deliberate change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check Your Lockfile Right Now
&lt;/h2&gt;

&lt;p&gt;If you use any of the four affected packages, check immediately:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'"laravel-lang/(lang|attributes|http-statuses|actions)"'&lt;/span&gt; composer.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any of those packages appear and you ran &lt;code&gt;composer update&lt;/code&gt; or a fresh install between 22–23 May 2026 UTC, treat the host as compromised. Rotate all secrets that were accessible from the build environment.&lt;/p&gt;

&lt;p&gt;Indicator of compromise: outbound HTTP/S connections from your build machines or containers to &lt;code&gt;flipboxstudio.info&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;autoload.files&lt;/code&gt; Threat Surface
&lt;/h2&gt;

&lt;p&gt;The attack vector — &lt;code&gt;autoload.files&lt;/code&gt; — deserves specific attention. Composer's &lt;code&gt;autoload.files&lt;/code&gt; section lists PHP files that are included on every request via the generated &lt;code&gt;vendor/autoload.php&lt;/code&gt;. Unlike PSR-4 classes that only load when referenced, these files execute unconditionally as part of the autoloader bootstrap.&lt;/p&gt;

&lt;p&gt;Legitimate packages use &lt;code&gt;autoload.files&lt;/code&gt; for global helper functions (Laravel itself uses it for &lt;code&gt;Illuminate/Support/helpers.php&lt;/code&gt;). Attackers use it for the same reason: guaranteed execution with no trigger required.&lt;/p&gt;

&lt;p&gt;When auditing third-party packages, pay attention to any package that declares &lt;code&gt;autoload.files&lt;/code&gt;. You can audit yours:&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;# List all packages using autoload.files&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;composer.lock | php &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
$lock = json_decode(file_get_contents("php://stdin"), true);
foreach ($lock["packages"] as $pkg) {
    if (!empty($pkg["autoload"]["files"])) {
        echo $pkg["name"] . "\n";
        foreach ($pkg["autoload"]["files"] as $f) {
            echo "  " . $f . "\n";
        }
    }
}
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review the output. Any unfamiliar file in an &lt;code&gt;autoload.files&lt;/code&gt; entry is worth reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running &lt;code&gt;composer audit&lt;/code&gt; in CI
&lt;/h2&gt;

&lt;p&gt;Composer 2.4 introduced the &lt;code&gt;audit&lt;/code&gt; command. It reads &lt;code&gt;composer.lock&lt;/code&gt; and checks every installed package against the PHP Security Advisories Database. This should be a required step in every CI pipeline.&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;# Run audit — exits non-zero if advisories are found&lt;/span&gt;
composer audit

&lt;span class="c"&gt;# JSON output for machine parsing&lt;/span&gt;
composer audit &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Composer 2.9 (released November 2025) went further and introduced &lt;code&gt;audit.block-insecure&lt;/code&gt;, which defaults to &lt;code&gt;true&lt;/code&gt;. This blocks &lt;code&gt;composer update&lt;/code&gt; operations when any installed package version has a known security advisory.&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;"config"&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;"audit"&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;"block-insecure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"block-abandoned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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="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;One important caveat: &lt;code&gt;composer audit&lt;/code&gt; checks packages against published CVEs. The &lt;code&gt;laravel-lang&lt;/code&gt; attack went undetected by &lt;code&gt;composer audit&lt;/code&gt; for the first several hours because no CVE existed yet. Dependency scanning catches known vulnerabilities. It does not catch novel supply chain attacks where a backdoor has not yet been catalogued. This is why egress controls matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Egress Controls in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;The most effective defence against credential exfiltration from a compromised package is preventing the exfiltration itself. GitHub Actions workflows run in network-accessible environments where every installed package can open outbound connections.&lt;/p&gt;

&lt;p&gt;StepSecurity's Harden-Runner adds network-level egress policy to your workflow jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;step-security/harden-runner@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;egress-policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;block&lt;/span&gt;
          &lt;span class="na"&gt;allowed-endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;packagist.org:443&lt;/span&gt;
            &lt;span class="s"&gt;repo.packagist.org:443&lt;/span&gt;
            &lt;span class="s"&gt;github.com:443&lt;/span&gt;
            &lt;span class="s"&gt;api.github.com:443&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

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

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run security audit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer audit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;egress-policy: block&lt;/code&gt;, any outbound connection not in the allowlist is blocked. A package attempting to reach &lt;code&gt;flipboxstudio.info&lt;/code&gt; would have been silently dropped before it could exfiltrate anything. Add this to your workflows before you need it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Static Analysis with Ward
&lt;/h2&gt;

&lt;p&gt;For deeper code-level inspection, Ward is a Go binary security scanner built specifically for Laravel projects. It runs 42+ built-in rules across secrets, injection, XSS, debug configuration, crypto, auth categories, and queries the OSV.dev vulnerability database in real time.&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;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;eljakani/tap/ward

&lt;span class="c"&gt;# Or download binary from GitHub releases&lt;/span&gt;
&lt;span class="c"&gt;# github.com/Eljakani/ward&lt;/span&gt;

&lt;span class="c"&gt;# Scan your project&lt;/span&gt;
ward scan /path/to/laravel-project

&lt;span class="c"&gt;# Export SARIF for GitHub Code Scanning integration&lt;/span&gt;
ward scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; sarif &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ward-results.sarif

&lt;span class="c"&gt;# Export JSON for automation&lt;/span&gt;
ward scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ward-results.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ward does not require Composer or access to a running deployment. The SARIF output integrates with GitHub's Code Scanning feature, surfacing findings as pull request annotations.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three CVEs You Must Patch
&lt;/h2&gt;

&lt;p&gt;The May 2026 attack was the headline, but three other vulnerabilities are equally urgent:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2025-54068 — Livewire RCE (Critical, CISA KEV)&lt;/strong&gt;&lt;br&gt;
Affects Livewire v3 through v3.6.3. An attacker can bypass the APP_KEY-signed checksum and achieve unauthenticated remote code execution via deserialization during component hydration. This does not require knowledge of your application key. CISA added it to the Known Exploited Vulnerabilities catalog, meaning it is being actively used in the wild.&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 your Livewire version&lt;/span&gt;
composer show livewire/livewire | &lt;span class="nb"&gt;grep &lt;/span&gt;versions

&lt;span class="c"&gt;# Upgrade&lt;/span&gt;
composer update livewire/livewire
&lt;span class="c"&gt;# Target: 3.6.4 or later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CVE-2026-39976 — Laravel Passport Authentication Bypass (CVSS 7.1)&lt;/strong&gt;&lt;br&gt;
Affects Passport 13.0.0 through 13.7.0. &lt;code&gt;TokenGuard&lt;/code&gt; does not verify whether the JWT &lt;code&gt;sub&lt;/code&gt; claim belongs to a user or a client. A machine token issued via &lt;code&gt;client_credentials&lt;/code&gt; can authenticate as a real user when integer IDs collide between the clients and users tables. Fixed in Passport 13.7.1.&lt;/p&gt;

&lt;p&gt;This only triggers if you use &lt;code&gt;EnsureClientIsResourceOwner&lt;/code&gt; middleware and have &lt;code&gt;Passport::$clientUuids&lt;/code&gt; set to &lt;code&gt;false&lt;/code&gt;. Check whether your Passport configuration matches this profile before concluding you are safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CVE-2025-27515 — Laravel File Validation Bypass (Moderate)&lt;/strong&gt;&lt;br&gt;
Affects wildcard file validation rules (e.g., &lt;code&gt;'files.*'&lt;/code&gt;) in Laravel Framework. Fixed in 10.48.29, 11.44.1, and 12.1.1. If your application accepts file uploads and validates them via wildcard rules, update your framework version.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Running &lt;code&gt;composer update&lt;/code&gt; in CI.&lt;/strong&gt; This rewrites &lt;code&gt;composer.lock&lt;/code&gt; and may pull in newly poisoned versions. CI must always run &lt;code&gt;composer install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not committing &lt;code&gt;composer.lock&lt;/code&gt;.&lt;/strong&gt; Without a committed lockfile you cannot detect whether a dependency changed between deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suppressing audit warnings.&lt;/strong&gt; &lt;code&gt;COMPOSER_NO_AUDIT=1&lt;/code&gt; silences real CVE alerts. Triage and resolve each advisory instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trusting &lt;code&gt;autoload.files&lt;/code&gt; entries blindly.&lt;/strong&gt; These files execute on every request — review them before including any package in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not rotating after a compromise.&lt;/strong&gt; If your build environment ran the poisoned packages, rotate all accessible secrets immediately: AWS keys, GitHub tokens, Stripe secrets, and your &lt;code&gt;APP_KEY&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Packagist's Response and Remaining Gaps
&lt;/h2&gt;

&lt;p&gt;Following the attack, Packagist implemented stable version immutability — published stable releases can no longer be overwritten, directly addressing the git tag rewrite vector.&lt;/p&gt;

&lt;p&gt;This protection applies only to packages on Packagist. Private forks, VCS repositories in &lt;code&gt;composer.json&lt;/code&gt;'s &lt;code&gt;repositories&lt;/code&gt; section, and &lt;code&gt;path&lt;/code&gt;-installed packages are not covered. If your project pulls from any non-Packagist source, integrity verification is your responsibility.&lt;/p&gt;

&lt;p&gt;Planned improvements include FIDO2 MFA for maintainers, organizational package ownership verification, and SLSA provenance with Sigstore attestations — none are in place today for all packages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification Checklist
&lt;/h2&gt;

&lt;p&gt;Run through this after making the changes above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;composer.lock&lt;/code&gt; is committed to version control&lt;/li&gt;
&lt;li&gt;CI pipeline runs &lt;code&gt;composer install&lt;/code&gt;, never &lt;code&gt;composer update&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;composer audit&lt;/code&gt; runs in CI and exits non-zero on findings&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;audit.block-insecure: true&lt;/code&gt; is set in &lt;code&gt;composer.json&lt;/code&gt; (Composer 2.9+)&lt;/li&gt;
&lt;li&gt;Egress controls are applied to CI workflow jobs&lt;/li&gt;
&lt;li&gt;Livewire is at 3.6.4 or later&lt;/li&gt;
&lt;li&gt;Laravel Passport (if used) is at 13.7.1 or later&lt;/li&gt;
&lt;li&gt;Laravel Framework is at 10.48.29, 11.44.1, or 12.1.1+ for file validation fix&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;autoload.files&lt;/code&gt; entries in third-party packages have been reviewed&lt;/li&gt;
&lt;li&gt;Network logs checked for any connections to &lt;code&gt;flipboxstudio.info&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;For a broader look at Composer security hardening, dependency scanning with Enlightn, and application-level scanning with StackShield, read the &lt;a href="https://www.mumbaiwebdesigner.com/blog/laravel-security-guide" rel="noopener noreferrer"&gt;Laravel Security Guide: Supply-Chain Risks, Composer, and Application Scanning&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you need Laravel development in Mumbai, &lt;a href="https://mumbaiwebdesigner.com/services/laravel-development-mumbai" rel="noopener noreferrer"&gt;Mumbai Web Designer&lt;/a&gt; builds production-grade Laravel applications.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>security</category>
      <category>composer</category>
    </item>
  </channel>
</rss>
