<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/vendor/feed/atom.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
                        <id>https://freek.dev/feed</id>
                                <link href="https://freek.dev/feed" rel="self"></link>
                                <title><![CDATA[freek.dev - all blogposts]]></title>
                    
                                <subtitle>All blogposts on freek.dev</subtitle>
                                                    <updated>2026-04-06T14:33:26+02:00</updated>
                        <entry>
            <title><![CDATA[My Take on Vibe Coding VS Agentic Engineering]]></title>
            <link rel="alternate" href="https://freek.dev/3059-my-take-on-vibe-coding-vs-agentic-engineering" />
            <id>https://freek.dev/3059</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>My thoughts on why Agentic Engineering is a better path than Vibe Coding, and the workflow I use to turn AI agents into a structured engineering process.</p>


<a href='https://wendelladriel.com/blog/my-take-on-vibe-coding-vs-agentic-engineering'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-06T14:33:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[SlideWire: Build Presentations with Livewire and Blade]]></title>
            <link rel="alternate" href="https://freek.dev/3056-slidewire-build-presentations-with-livewire-and-blade" />
            <id>https://freek.dev/3056</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>SlideWire is a Laravel package for building browser-based presentation decks using Livewire components and Blade templates. It comes with built-in navigation, transitions, syntax highlighting, and Mermaid diagrams.</p>


<a href='https://laravel-news.com/slidewire-build-presentations-with-livewire-and-blade'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-04T12:30:31+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Under the Hood: How Blaze Speeds Up Blade Templates]]></title>
            <link rel="alternate" href="https://freek.dev/3055-under-the-hood-how-blaze-speeds-up-blade-templates" />
            <id>https://freek.dev/3055</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A deep dive into how Blaze works internally. Matt Stauffer builds two toy versions from scratch to show how Blaze shifts Blade component rendering from runtime to compile time.</p>


<a href='https://tighten.com/insights/blaze-under-the-hood'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-03T12:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Context Anchoring]]></title>
            <link rel="alternate" href="https://freek.dev/3053-context-anchoring" />
            <id>https://freek.dev/3053</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Martin Fowler explores why AI coding sessions degrade over time and how externalizing decisions into structured documents keeps context reliable across sessions.</p>


<a href='https://martinfowler.com/articles/reduce-friction-ai/context-anchoring.html'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-02T12:30:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building Multi-Agent Workflows with the Laravel AI SDK]]></title>
            <link rel="alternate" href="https://freek.dev/3051-building-multi-agent-workflows-with-the-laravel-ai-sdk" />
            <id>https://freek.dev/3051</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>The Laravel blog walks through how to implement the five multi-agent patterns from Anthropic's &quot;Building Effective Agents&quot; research using the Laravel AI SDK. Prompt chaining, parallelization, routing, orchestrator-workers, and evaluator-optimizer loops, all built with just the agent() helper.</p>


<a href='https://laravel.com/blog/building-multi-agent-workflows-with-the-laravel-ai-sdk'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-01T14:30:05+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Agent responsibly]]></title>
            <link rel="alternate" href="https://freek.dev/3069-agent-responsibly" />
            <id>https://freek.dev/3069</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Vercel shares their internal framework for shipping agent-generated code safely. The core argument: green CI is no longer proof of safety, because agents produce code that looks flawless while remaining blind to production realities. The post outlines how to build systems where agents can act with high autonomy because deployment is safe by default.</p>


<a href='https://vercel.com/blog/agent-responsibly'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-01T12:30:27+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Zen of AI Coding]]></title>
            <link rel="alternate" href="https://freek.dev/3050-zen-of-ai-coding" />
            <id>https://freek.dev/3050</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A thoughtful collection of principles for working with coding agents, inspired by the Zen of Python. Covers how cheap code changes prioritization, why refactoring and repaying tech debt got easier, and why your role shifts from typing code to framing problems.</p>


<a href='https://nonstructured.com/zen-of-ai-coding/'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-31T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introduction to Delegated Types]]></title>
            <link rel="alternate" href="https://freek.dev/3013-introduction-to-delegated-types" />
            <id>https://freek.dev/3013</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>This post explores what Delegated Types are, highlights their benefits, compares them to alternatives like Single-Table Inheritance (STI), and examines practical use cases they enable.</p>


<a href='https://tighten.com/insights/delegated-types/'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-30T12:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ Scotty: a beautiful SSH task runner]]></title>
            <link rel="alternate" href="https://freek.dev/3064-scotty-a-beautiful-ssh-task-runner" />
            <id>https://freek.dev/3064</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>We just released <a href="https://github.com/spatie/scotty">Scotty</a>, a beautiful SSH task runner. It lets you define deploy scripts and other remote tasks, run them from your terminal, and watch every step as it happens. It supports both Laravel Envoy's Blade format and a new plain bash format.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-run-deploy.jpg?raw=true" alt="Deploy output" /></p>
<h2 id="why-we-built-scotty">Why we built Scotty</h2>
<p>Even though services like Laravel Cloud make it possible to never think about servers again, I still prefer deploying to my own servers for some projects. I know my way around them, I can pick whichever server provider I want, and I have full control over the environment.</p>
<p>Tools like <a href="https://forge.laravel.com">Laravel Forge</a> have built-in deploy functionality, and it works well. But I've always preferred running deploy scripts manually from my terminal. I want to see exactly what's happening while it runs, and when something goes wrong, I want to be staring at the output the moment it happens, not clicking through a web UI to find a log.</p>
<p>For years, I used <a href="https://laravel.com/docs/envoy">Laravel Envoy</a> for this. It does the job, but I wanted nicer output while tasks are running, and the ability to pause execution mid-deploy. Envoy uses a Blade-based file format, which works fine for us. But not everyone wants to write Blade syntax for their deploy scripts. So Scotty also offers a plain bash format for people who prefer that.</p>
<p>Scotty was built with the help of AI, using the <a href="https://github.com/laravel/envoy">Envoy codebase</a> as a source. Even though Scotty is a rebuild from scratch, in spirit it is a fork of Envoy. Credits to Laravel for providing the foundation. You can read the <a href="https://github.com/spatie/scotty#acknowledgements">full acknowledgement in the Scotty repo</a>.</p>
<h2 id="using-scotty">Using Scotty</h2>
<h3 id="defining-tasks-and-deploying">Defining tasks and deploying</h3>
<p>You define your tasks in a <code>Scotty.sh</code> file. It's just plain bash with some annotation comments. Your editor highlights it correctly, you get full shell support out of the box. Here's what that looks like:</p>
<pre data-lang="bash" class="notranslate"><span class="hl-comment">#!/usr/bin/env scotty</span>

<span class="hl-comment"># @servers remote=deployer@your-server.com</span>

<span class="hl-comment"># @task on:remote</span>
deploy() {
    cd /var/www/my-app
    git pull origin main
    php artisan migrate --force
}
</pre>
<p>Deploy with a single command:</p>
<pre data-lang="bash" class="notranslate">scotty run deploy
</pre>
<p>Scotty connects to your server, runs each task in order, and shows you what's happening in real time. Each task displays its name, a step counter, elapsed time, and the command currently being executed. When everything finishes, you get a summary table with timing for each step.</p>
<p>These screenshots are from deploying this very blog. You can find the <a href="https://github.com/spatie/freek.dev/blob/main/Envoy.blade.php">deploy file on GitHub</a>.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-run-deploy.jpg?raw=true" alt="Deploy output" /></p>
<p>If a task fails, its output is shown and execution stops right there so you can investigate.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-failure.jpg?raw=true" alt="Task failure" /></p>
<h3 id="writing-plain-bash-instead-of-blade">Writing plain bash instead of Blade</h3>
<p>In addition to being able to read <code>Envoy.blade.php</code> files, Scotty introduces a new <code>Scotty.sh</code> format. Every line is real bash. Tasks are just bash functions with a <code># @task</code> annotation that tells Scotty which server to run them on. Macros group tasks into a sequence. Variables are plain bash variables, available everywhere.</p>
<p>Because it's all valid bash, you can use helper functions, computed values like <code>$(date +%Y%m%d-%H%M%S)</code>, and any shell construct you'd normally use. No Blade directives, no PHP <code>@setup</code> blocks, no <code>{{ $variable }}</code> interpolation.</p>
<p>You can also pass variables from the command line. Running <code>scotty run deploy --branch=develop</code> makes <code>$BRANCH</code> available in your tasks. The key is automatically uppercased.</p>
<p>You can list all available tasks and macros with <code>scotty tasks</code>:</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-tasks.jpg?raw=true" alt="scotty tasks" /></p>
<h3 id="pausing-resuming-and-pretending">Pausing, resuming, and pretending</h3>
<p>You can press <code>p</code> at any time during execution to pause after the current task finishes. Press <code>Enter</code> to resume, or <code>Ctrl+C</code> to cancel. This is handy when you want to check something on the server mid-deploy before continuing.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-pause.jpg?raw=true" alt="Pause and resume" /></p>
<p>There's also a pretend mode (<code>scotty run deploy --pretend</code>) that shows you exactly which SSH commands would be executed without actually running them. And a summary mode (<code>scotty run deploy --summary</code>) that hides task output and only shows results.</p>
<h3 id="validating-your-setup-with-doctor">Validating your setup with doctor</h3>
<p>Before your first deploy to a new server, you can run <code>scotty doctor</code> to validate your setup. It checks that your file parses correctly, verifies SSH connectivity to each server, and confirms that tools like <code>php</code>, <code>composer</code>, <code>node</code>, and <code>git</code> are available on the remote machine.</p>
<p><img src="https://github.com/spatie/scotty/blob/main/docs/images/scotty-doctor.jpg?raw=true" alt="scotty doctor" /></p>
<h3 id="migrating-from-envoy">Migrating from Envoy</h3>
<p>If you're already using Laravel Envoy, Scotty can read your <code>Envoy.blade.php</code> file directly. No changes needed. Just run <code>scotty run deploy</code> and it works. From there, you can gradually migrate to the <code>Scotty.sh</code> format if you want to, or keep using Blade.</p>
<p>If you're new to all of this, the <a href="https://spatie.be/docs/scotty/v1/basic-usage/your-first-deploy-script">Your first deploy script</a> page in the docs walks you through everything step by step.</p>
<h2 id="in-closing">In closing</h2>
<p>Scotty gives you a clean, modern way to run deploy scripts and other SSH tasks from your terminal. Plain bash, beautiful output, and full control over every step.</p>
<p>You can find <a href="https://spatie.be/docs/scotty/v1/introduction">the full documentation on our docs site</a> and the source code <a href="https://github.com/spatie/scotty">on GitHub</a>. This is one of the many tools and packages we've created at <a href="https://spatie.be">Spatie</a>. If you want to support our open source work, consider picking up one of <a href="https://spatie.be/products">our paid products</a>.</p>
]]>
            </summary>
                                    <updated>2026-03-30T09:44:16+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Generating OG images at the edge on Cloudflare]]></title>
            <link rel="alternate" href="https://freek.dev/3047-generating-og-images-at-the-edge-on-cloudflare" />
            <id>https://freek.dev/3047</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Matt Rothenberg walks through how to generate dynamic Open Graph images on Cloudflare Workers. A practical guide covering the full setup from rendering to caching.</p>


<a href='https://mattrothenberg.com/notes/edge-og-images/'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-27T13:30:04+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Speed up your Livewire tests]]></title>
            <link rel="alternate" href="https://freek.dev/3038-speed-up-your-livewire-tests" />
            <id>https://freek.dev/3038</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Michael Dyrynda found that switching from <code>fill()</code> to <code>set()</code> in Livewire tests reduced his test suite from 22 seconds to 4 seconds. The difference: <code>fill</code> triggers a Livewire round-trip per field, while <code>set</code> batches them into one.</p>


<a href='https://dyrynda.com.au/blog/speed-up-your-livewire-tests'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-26T13:30:29+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Liminal - A full Laravel playground in your browser]]></title>
            <link rel="alternate" href="https://freek.dev/3021-liminal-a-full-laravel-playground-in-your-browser" />
            <id>https://freek.dev/3021</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A static, free, and open-source Laravel playground that runs entirely in the browser! Comes with a sqlite db, artisan commands, two-way file syncing, github imports, and more.</p>


<a href='https://liminal.aschmelyun.com'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-25T13:30:28+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ What's new in laravel-activitylog v5]]></title>
            <link rel="alternate" href="https://freek.dev/3058-whats-new-in-laravel-activitylog-v5" />
            <id>https://freek.dev/3058</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>We just released v5 of <a href="https://spatie.be/docs/laravel-activitylog">laravel-activitylog</a>, our package for logging user activity and model events in Laravel.</p>
<p>In <a href="https://flareapp.io">Flare</a>, <a href="https://mailcoach.app">Mailcoach</a>, and <a href="https://ohdear.app">Oh Dear</a> we use it to build audit logs, so we can track what users are doing: who changed a setting, who deleted a project, who invited a team member. If you need something similar in your app, this package makes it easy.</p>
<p>This major release requires PHP 8.4+ and Laravel 12+, and brings a cleaner API, a better database schema, and customizable internals. Let me walk you through what the package can do and what's new in v5.</p>
<!--more-->
<h2 id="using-the-package">Using the package</h2>
<p>At its core, the package lets you log what happens in your application. The simplest usage looks like this:</p>
<pre data-lang="php" class="notranslate"><span class="hl-property">activity</span>()-&gt;<span class="hl-property">log</span>(<span class="hl-value">'Look mum, I logged something'</span>);
</pre>
<p>You can retrieve all logged activities using the <code>Activity</code> model:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\Activitylog\Models\Activity</span>;

<span class="hl-variable">$lastActivity</span> = <span class="hl-type">Activity</span>::<span class="hl-property">all</span>()-&gt;<span class="hl-property">last</span>();

<span class="hl-comment">// returns 'Look mum, I logged something'</span>
<span class="hl-variable">$lastActivity</span>-&gt;<span class="hl-property">description</span>;
</pre>
<p>Most of the time you want to know two things: what was affected, and who did it. The package calls these the subject and the causer. You can also attach custom properties for any extra context you need:</p>
<pre data-lang="php" class="notranslate"><span class="hl-property">activity</span>()
   -&gt;<span class="hl-property">performedOn</span>(<span class="hl-variable">$article</span>)
   -&gt;<span class="hl-property">causedBy</span>(<span class="hl-variable">$user</span>)
   -&gt;<span class="hl-property">withProperties</span>([<span class="hl-value">'via'</span> =&gt; <span class="hl-value">'admin-panel'</span>])
   -&gt;<span class="hl-property">log</span>(<span class="hl-value">'edited'</span>);

<span class="hl-variable">$lastActivity</span> = <span class="hl-type">Activity</span>::<span class="hl-property">all</span>()-&gt;<span class="hl-property">last</span>();

<span class="hl-comment">// the article that was edited</span>
<span class="hl-variable">$lastActivity</span>-&gt;<span class="hl-property">subject</span>;

<span class="hl-comment">// the user who edited it</span>
<span class="hl-variable">$lastActivity</span>-&gt;<span class="hl-property">causer</span>;

<span class="hl-comment">// 'admin-panel'</span>
<span class="hl-variable">$lastActivity</span>-&gt;<span class="hl-property">getProperty</span>(<span class="hl-value">'via'</span>);
</pre>
<p>The <code>Activity</code> model provides query scopes to filter your activity log:</p>
<pre data-lang="php" class="notranslate"><span class="hl-type">Activity</span>::<span class="hl-property">forSubject</span>(<span class="hl-variable">$newsItem</span>)-&gt;<span class="hl-property">get</span>();
<span class="hl-type">Activity</span>::<span class="hl-property">causedBy</span>(<span class="hl-variable">$user</span>)-&gt;<span class="hl-property">get</span>();
<span class="hl-type">Activity</span>::<span class="hl-property">forEvent</span>(<span class="hl-value">'updated'</span>)-&gt;<span class="hl-property">get</span>();
<span class="hl-type">Activity</span>::<span class="hl-property">inLog</span>(<span class="hl-value">'payment'</span>)-&gt;<span class="hl-property">get</span>();

<span class="hl-comment">// or combine them</span>
<span class="hl-type">Activity</span>::<span class="hl-property">forSubject</span>(<span class="hl-variable">$newsItem</span>)
    -&gt;<span class="hl-property">causedBy</span>(<span class="hl-variable">$user</span>)
    -&gt;<span class="hl-property">forEvent</span>(<span class="hl-value">'updated'</span>)
    -&gt;<span class="hl-property">get</span>();
</pre>
<h3 id="automatic-model-event-logging">Automatic model event logging</h3>
<p>Imagine you want to track whenever a model is created, updated, or deleted. Just add the <code>LogsActivity</code> trait to your model. In v5, that's all you need for basic logging:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Database\Eloquent\Model</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Activitylog\Models\Concerns\LogsActivity</span>;

<span class="hl-keyword">class</span> <span class="hl-type">NewsItem</span> <span class="hl-keyword">extends</span> <span class="hl-type">Model</span>
{
    <span class="hl-keyword">use</span> <span class="hl-type">LogsActivity</span>;
}
</pre>
<p>That's it. No <code>getActivitylogOptions()</code> method needed. Now imagine you also want to track which attributes changed. Override the method and tell it what to watch:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\Activitylog\Support\LogOptions</span>;

<span class="hl-keyword">class</span> <span class="hl-type">NewsItem</span> <span class="hl-keyword">extends</span> <span class="hl-type">Model</span>
{
    <span class="hl-keyword">use</span> <span class="hl-type">LogsActivity</span>;

    <span class="hl-keyword">protected</span> <span class="hl-property">$fillable</span> = [<span class="hl-value">'name'</span>, <span class="hl-value">'text'</span>];

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">getActivitylogOptions</span>(): <span class="hl-type">LogOptions</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-type">LogOptions</span>::<span class="hl-property">defaults</span>()
            -&gt;<span class="hl-property">logOnly</span>([<span class="hl-value">'name'</span>, <span class="hl-value">'text'</span>]);
    }
}
</pre>
<p>Now when you update a news item, the package tracks exactly what changed:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$newsItem</span>-&gt;<span class="hl-property">name</span> = <span class="hl-value">'updated name'</span>;
<span class="hl-variable">$newsItem</span>-&gt;<span class="hl-property">save</span>();

<span class="hl-variable">$activity</span> = <span class="hl-type">Activity</span>::<span class="hl-property">all</span>()-&gt;<span class="hl-property">last</span>();
<span class="hl-variable">$activity</span>-&gt;<span class="hl-property">attribute_changes</span>;
<span class="hl-comment">// [</span>
<span class="hl-comment">//     'attributes' =&gt; [</span>
<span class="hl-comment">//         'name' =&gt; 'updated name',</span>
<span class="hl-comment">//         'text' =&gt; 'Lorem',</span>
<span class="hl-comment">//     ],</span>
<span class="hl-comment">//     'old' =&gt; [</span>
<span class="hl-comment">//         'name' =&gt; 'original name',</span>
<span class="hl-comment">//         'text' =&gt; 'Lorem',</span>
<span class="hl-comment">//     ],</span>
<span class="hl-comment">// ]</span>
</pre>
<p>You can log all fillable attributes with <code>logFillable()</code>, all unguarded attributes with <code>logUnguarded()</code>, or use <code>logAll()</code> combined with <code>logExcept()</code> to log everything except sensitive fields like passwords.</p>
<p>If you only want to see what actually changed rather than all tracked attributes, chain <code>logOnlyDirty()</code>.</p>
<h3 id="running-code-before-an-activity-is-saved">Running code before an activity is saved</h3>
<p>When a model event is logged, you can hook into the process by defining a <code>beforeActivityLogged()</code> method on your model:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">class</span> <span class="hl-type">NewsItem</span> <span class="hl-keyword">extends</span> <span class="hl-type">Model</span>
{
    <span class="hl-keyword">use</span> <span class="hl-type">LogsActivity</span>;

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">beforeActivityLogged</span>(<span class="hl-injection"><span class="hl-type">Activity</span> $activity, <span class="hl-type">string</span> $eventName</span>): <span class="hl-type">void</span>
    {
        <span class="hl-variable">$activity</span>-&gt;<span class="hl-property">properties</span> = <span class="hl-variable">$activity</span>-&gt;<span class="hl-property">properties</span>-&gt;<span class="hl-property">merge</span>([
            <span class="hl-value">'ip_address'</span> =&gt; <span class="hl-property">request</span>()-&gt;<span class="hl-property">ip</span>(),
        ]);
    }
}
</pre>
<p>This runs right before the activity is persisted, giving you a chance to enrich it with extra data.</p>
<h3 id="customizable-action-classes">Customizable action classes</h3>
<p>The core operations of the package (logging activities and cleaning old records) are now handled by action classes. You can extend these and swap them in via config.</p>
<p>For example, say you want to save activities to the queue instead of writing them to the database during the request. Extend the action and override the <code>save()</code> method:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\Activitylog\Actions\LogActivityAction</span>;

<span class="hl-keyword">class</span> <span class="hl-type">QueuedLogAction</span> <span class="hl-keyword">extends</span> <span class="hl-type">LogActivityAction</span>
{
    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">save</span>(<span class="hl-injection"><span class="hl-type">Model</span> $activity</span>): <span class="hl-type">void</span>
    {
        <span class="hl-property">dispatch</span>(<span class="hl-keyword">new</span> <span class="hl-type">SaveActivityJob</span>(<span class="hl-variable">$activity</span>-&gt;<span class="hl-property">toArray</span>()));
    }
}
</pre>
<p>Then tell the package to use your custom action in <code>config/activitylog.php</code>:</p>
<pre data-lang="php" class="notranslate"><span class="hl-value">'actions'</span> =&gt; [
    <span class="hl-value">'log_activity'</span> =&gt; <span class="hl-type">QueuedLogAction</span>::<span class="hl-keyword">class</span>,
],
</pre>
<p>You can also override <code>transformChanges()</code> to manipulate the changes array before saving. Here's an example that redacts password changes so they never end up in your activity log:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Database\Eloquent\Model</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Support\Arr</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Activitylog\Actions\LogActivityAction</span>;

<span class="hl-keyword">class</span> <span class="hl-type">RedactSensitiveFieldsAction</span> <span class="hl-keyword">extends</span> <span class="hl-type">LogActivityAction</span>
{
    <span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">transformChanges</span>(<span class="hl-injection"><span class="hl-type">Model</span> $activity</span>): <span class="hl-type">void</span>
    {
        <span class="hl-variable">$changes</span> = <span class="hl-variable">$activity</span>-&gt;<span class="hl-property">attribute_changes</span>?-&gt;<span class="hl-property">toArray</span>() ?? [];

        <span class="hl-type">Arr</span>::<span class="hl-property">forget</span>(<span class="hl-variable">$changes</span>, [<span class="hl-value">'attributes.password'</span>, <span class="hl-value">'old.password'</span>]);

        <span class="hl-variable">$activity</span>-&gt;<span class="hl-property">attribute_changes</span> = <span class="hl-property">collect</span>(<span class="hl-variable">$changes</span>);
    }
}
</pre>
<h3 id="buffering-activities">Buffering activities</h3>
<p>Say you have an endpoint that updates product prices in bulk. Each product update triggers a model event, and each model event logs an activity. With 200 products, that's 200 <code>INSERT</code> queries just for activity logging.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">foreach</span> (<span class="hl-variable">$products</span> <span class="hl-keyword">as</span> <span class="hl-variable">$product</span>) {
    <span class="hl-comment">// each update triggers an activity INSERT query</span>
    <span class="hl-variable">$product</span>-&gt;<span class="hl-property">update</span>([<span class="hl-value">'price'</span> =&gt; <span class="hl-variable">$newPrices</span>[<span class="hl-variable">$product</span>-&gt;<span class="hl-property">id</span>]]);
}
</pre>
<p>With buffering enabled, the package collects all those activities in memory during the request and inserts them in a single bulk query after the response has been sent to the client. Your user gets a fast response, and the database does one insert instead of 200.</p>
<p>Buffering is off by default. You can enable it in the <code>config/activitylog.php</code> config file. No other code changes needed. All existing logging code (both automatic model event logging and manual <code>activity()-&gt;log()</code> calls) will be buffered automatically.</p>
<p>Under the hood, the <code>ActivityBuffer</code> class collects activities in an array and inserts them all at once when flushed:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">class</span> <span class="hl-type">ActivityBuffer</span>
{
    <span class="hl-keyword">protected</span> <span class="hl-type">array</span> <span class="hl-property">$pending</span> = [];

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">add</span>(<span class="hl-injection"><span class="hl-type">Model</span> $activity</span>): <span class="hl-type">void</span>
    {
        <span class="hl-variable">$this</span>-&gt;<span class="hl-property">pending</span>[] = <span class="hl-variable">$this</span>-&gt;<span class="hl-property">prepareForInsert</span>(<span class="hl-variable">$activity</span>);
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">flush</span>(): <span class="hl-type">void</span>
    {
        <span class="hl-keyword">if</span> (<span class="hl-keyword">empty</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">pending</span>)) {
            <span class="hl-keyword">return</span>;
        }

        <span class="hl-variable">$modelClass</span> = <span class="hl-type">Config</span>::<span class="hl-property">activityModel</span>();
        <span class="hl-variable">$modelClass</span>::<span class="hl-property">query</span>()-&gt;<span class="hl-property">insert</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">pending</span>);

        <span class="hl-variable">$this</span>-&gt;<span class="hl-property">pending</span> = [];
    }
}
</pre>
<p>The service provider takes care of flushing the buffer at the right time:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">protected</span> <span class="hl-keyword">function</span> <span class="hl-property">registerActivityBufferFlushing</span>(): <span class="hl-type">void</span>
{
    <span class="hl-comment">// flush after the response has been sent</span>
    <span class="hl-variable">$this</span>-&gt;<span class="hl-property">app</span>-&gt;<span class="hl-property">terminating</span>(<span class="hl-keyword">fn</span> () =&gt; <span class="hl-property">app</span>(<span class="hl-type">ActivityBuffer</span>::<span class="hl-keyword">class</span>)-&gt;<span class="hl-property">flush</span>());

    <span class="hl-comment">// flush after each queued job</span>
    <span class="hl-variable">$this</span>-&gt;<span class="hl-property">app</span>[<span class="hl-value">'events'</span>]-&gt;<span class="hl-property">listen</span>(
        [<span class="hl-type">JobProcessed</span>::<span class="hl-keyword">class</span>, <span class="hl-type">JobFailed</span>::<span class="hl-keyword">class</span>],
        <span class="hl-keyword">fn</span> () =&gt; <span class="hl-property">app</span>(<span class="hl-type">ActivityBuffer</span>::<span class="hl-keyword">class</span>)-&gt;<span class="hl-property">flush</span>(),
    );

    <span class="hl-comment">// safety net if the application terminates unexpectedly</span>
    <span class="hl-property">register_shutdown_function</span>(<span class="hl-injection">function (</span>) {
        <span class="hl-keyword">try</span> {
            <span class="hl-property">app</span>(<span class="hl-type">ActivityBuffer</span>::<span class="hl-keyword">class</span>)-&gt;<span class="hl-property">flush</span>();
        } <span class="hl-keyword">catch</span> (\Throwable) {
        }
    });
}
</pre>
<p>One thing to be aware of: buffered activities won't have a database ID until the buffer is flushed. If you need to read back the activity ID immediately after logging, don't enable buffering.</p>
<p>This works with Octane (the buffer is a scoped binding, so it resets between requests) and queues out of the box.</p>
<h2 id="in-closing">In closing</h2>
<p>v5 doesn't bring a lot of new features, but it modernizes the package, cleans up the internals, and makes the things that were hard to customize in v4 easy to swap out. If you're upgrading from v4, be aware that there are quite a few breaking changes. Check the <a href="https://github.com/spatie/laravel-activitylog/blob/v5/UPGRADING.md">upgrade guide</a> for the full list.</p>
<p>You can find the complete documentation at <a href="https://spatie.be/docs/laravel-activitylog">spatie.be/docs/laravel-activitylog</a> and the source code <a href="https://github.com/spatie/laravel-activitylog">on GitHub</a>.</p>
<p>This is one of the many packages we've created at <a href="https://spatie.be/open-source">Spatie</a>. If you want to support our open source work, consider picking up one of our <a href="https://spatie.be/products">paid products</a>.</p>
]]>
            </summary>
                                    <updated>2026-03-25T11:39:31+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Rethinking our frontend future at Spatie]]></title>
            <link rel="alternate" href="https://freek.dev/3025-rethinking-our-frontend-future-at-spatie" />
            <id>https://freek.dev/3025</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Nick wrote about how native browser features like the dialog element, the Popover API, and anchor positioning could eventually replace much of what we use shadcn and Radix for at Spatie today. A thoughtful look at where the web platform is heading and what it means for our frontend stack.</p>


<a href='https://spatie.be/blog/rethinking-our-frontend-future-at-spatie'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-24T13:30:30+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Moving from PHPStorm to Zed for Laravel development]]></title>
            <link rel="alternate" href="https://freek.dev/3020-moving-from-phpstorm-to-zed-for-laravel-development" />
            <id>https://freek.dev/3020</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Chris Mellor wrote a practical guide on setting up Zed as a Laravel IDE, covering PHP extensions, Pint formatting, Blade support, and how it compares to PHPStorm.</p>


<a href='https://x.com/cmellor/status/2024109224146440404'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-23T13:30:28+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Chat with your documents: a practical guide to RAG using the Laravel AI SDK]]></title>
            <link rel="alternate" href="https://freek.dev/3019-chat-with-your-documents-a-practical-guide-to-rag-using-the-laravel-ai-sdk" />
            <id>https://freek.dev/3019</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A thorough walkthrough of building a RAG system in Laravel using the new AI SDK, Postgres for vector storage, and Livewire 4 for a streaming chat UI. Covers everything from what RAG is and how semantic search works to embedding documents and querying them.</p>


<a href='https://tighten.com/insights/chat-with-your-documents-a-practical-guide-to-rag-using-the-new-laravel-ai-sdk/'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-20T13:30:26+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Build an MCP server with Laravel]]></title>
            <link rel="alternate" href="https://freek.dev/3018-build-an-mcp-server-with-laravel" />
            <id>https://freek.dev/3018</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Daniel Coulbourne walks through building an MCP server with the official laravel/mcp package. He built one for his blog in about 20 minutes, then used it to write and publish the post you're reading.</p>


<a href='https://thunk.dev/posts/build-mcp-server-with-laravel'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-19T13:30:29+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Run seeders from migrations]]></title>
            <link rel="alternate" href="https://freek.dev/3046-run-seeders-from-migrations" />
            <id>https://freek.dev/3046</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A practical look at why you should populate essential data from migrations instead of seeders. Once your app is live, manually running seeders becomes a deployment risk. Migrations are deterministic, automatic, and roll back cleanly.</p>


<a href='https://spatie.be/blog/run-seeders-from-migrations'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-18T13:30:30+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[The Agentic Artisan]]></title>
            <link rel="alternate" href="https://freek.dev/3045-the-agentic-artisan" />
            <id>https://freek.dev/3045</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Christoph Rumpel reflects on how AI tools are changing the way developers work. The core message: take the shortcuts that cut out mechanical work, but don't outsource the parts that make your work yours.</p>


<a href='https://christoph-rumpel.com/2026/3/the-agentic-artisan'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-17T13:30:27+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ Laravel Query Builder v7: a must-have package for building APIs in Laravel]]></title>
            <link rel="alternate" href="https://freek.dev/3052-laravel-query-builder-v7-a-must-have-package-for-building-apis-in-laravel" />
            <id>https://freek.dev/3052</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>We just released v7 of <a href="https://github.com/spatie/laravel-query-builder">spatie/laravel-query-builder</a>, our package that makes it easy to build flexible API endpoints. If you're building an API with Laravel, you'll almost certainly need to let consumers filter results, sort them, include relationships and select specific fields. Writing that logic by hand for every endpoint gets repetitive fast, and it's easy to accidentally expose columns or relationships you didn't intend to.</p>
<p>Our query builder takes care of all of that. It reads query parameters from the URL, translates them into the right Eloquent queries, and makes sure only the things you've explicitly allowed can be queried.</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// GET /users?filter[name]=John&amp;include=posts&amp;sort=-created_at</span>

<span class="hl-variable">$users</span> = <span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">allowedFilters</span>(<span class="hl-value">'name'</span>)
    -&gt;<span class="hl-property">allowedIncludes</span>(<span class="hl-value">'posts'</span>)
    -&gt;<span class="hl-property">allowedSorts</span>(<span class="hl-value">'created_at'</span>)
    -&gt;<span class="hl-property">get</span>();

<span class="hl-comment">// select * from users where name = 'John' order by created_at desc</span>
</pre>
<p>This major version requires PHP 8.3+ and Laravel 12 or higher, and brings a cleaner API along with some features we've been wanting to add for a while.</p>
<p>Let me walk you through how the package works and what's new.</p>
<!--more-->
<h2 id="using-the-package">Using the package</h2>
<p>The idea is simple: your API consumers pass query parameters in the URL, and the package translates those into the right Eloquent query. You just define what's allowed.</p>
<p>Say you have a <code>User</code> model and you want to let API consumers filter by name. Here's all you need:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\QueryBuilder\QueryBuilder</span>;

<span class="hl-variable">$users</span> = <span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">allowedFilters</span>(<span class="hl-value">'name'</span>)
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>Now when someone requests <code>/users?filter[name]=John</code>, the package adds the appropriate <code>WHERE</code> clause to the query:</p>
<pre data-lang="sql" class="notranslate"><span class="hl-keyword">select</span> * <span class="hl-keyword">from</span> <span class="hl-type">users</span> <span class="hl-keyword">where</span> name = '<span class="hl-value">John</span>'
</pre>
<p>Only the filters you've explicitly allowed will work. If someone tries <code>/users?filter[secret_column]=something</code>, the package throws an <code>InvalidFilterQuery</code> exception. Your database schema stays hidden from API consumers.</p>
<p>You can allow multiple filters at once and combine them with sorting:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$users</span> = <span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">allowedFilters</span>(<span class="hl-value">'name'</span>, <span class="hl-value">'email'</span>)
    -&gt;<span class="hl-property">allowedSorts</span>(<span class="hl-value">'name'</span>, <span class="hl-value">'created_at'</span>)
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>A request to <code>/users?filter[name]=John&amp;sort=-created_at</code> now filters by name and sorts by <code>created_at</code> descending (the <code>-</code> prefix means descending).</p>
<p>Including relationships works the same way. If you want consumers to be able to eager-load a user's posts:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$users</span> = <span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">allowedFilters</span>(<span class="hl-value">'name'</span>, <span class="hl-value">'email'</span>)
    -&gt;<span class="hl-property">allowedIncludes</span>(<span class="hl-value">'posts'</span>, <span class="hl-value">'permissions'</span>)
    -&gt;<span class="hl-property">allowedSorts</span>(<span class="hl-value">'name'</span>, <span class="hl-value">'created_at'</span>)
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>A request to <code>/users?include=posts&amp;filter[name]=John&amp;sort=-created_at</code> now returns users named John, sorted by creation date, with their posts eager-loaded.</p>
<p>You can also select specific fields to keep your responses lean:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$users</span> = <span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">allowedFields</span>(<span class="hl-value">'id'</span>, <span class="hl-value">'name'</span>, <span class="hl-value">'email'</span>)
    -&gt;<span class="hl-property">allowedIncludes</span>(<span class="hl-value">'posts'</span>)
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>With <code>/users?fields=id,email&amp;include=posts</code>, only the <code>id</code> and <code>email</code> columns are selected.</p>
<p>The <code>QueryBuilder</code> extends Laravel's default Eloquent builder, so all your favorite methods still work. You can combine it with existing queries:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$query</span> = <span class="hl-type">User</span>::<span class="hl-property">where</span>(<span class="hl-value">'active'</span>, <span class="hl-keyword">true</span>);

<span class="hl-variable">$users</span> = <span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-variable">$query</span>)
    -&gt;<span class="hl-property">withTrashed</span>()
    -&gt;<span class="hl-property">allowedFilters</span>(<span class="hl-value">'name'</span>)
    -&gt;<span class="hl-property">allowedIncludes</span>(<span class="hl-value">'posts'</span>, <span class="hl-value">'permissions'</span>)
    -&gt;<span class="hl-property">where</span>(<span class="hl-value">'score'</span>, <span class="hl-value">'&gt;'</span>, 42)
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>The query parameter names follow the <a href="http://jsonapi.org/">JSON API specification</a> as closely as possible. This means you get a consistent, well-documented API surface without having to think about naming conventions.</p>
<h2 id="whats-new-in-v7">What's new in v7</h2>
<h3 id="variadic-parameters">Variadic parameters</h3>
<p>All the <code>allowed*</code> methods now accept variadic arguments instead of arrays.</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// Before (v6)</span>
<span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">allowedFilters</span>([<span class="hl-value">'name'</span>, <span class="hl-value">'email'</span>])
    -&gt;<span class="hl-property">allowedSorts</span>([<span class="hl-value">'name'</span>])
    -&gt;<span class="hl-property">allowedIncludes</span>([<span class="hl-value">'posts'</span>]);

<span class="hl-comment">// After (v7)</span>
<span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">allowedFilters</span>(<span class="hl-value">'name'</span>, <span class="hl-value">'email'</span>)
    -&gt;<span class="hl-property">allowedSorts</span>(<span class="hl-value">'name'</span>)
    -&gt;<span class="hl-property">allowedIncludes</span>(<span class="hl-value">'posts'</span>);
</pre>
<p>If you have a dynamic list, use the spread operator:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$filters</span> = [<span class="hl-value">'name'</span>, <span class="hl-value">'email'</span>];
<span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)-&gt;<span class="hl-property">allowedFilters</span>(...<span class="hl-variable">$filters</span>);
</pre>
<h3 id="aggregate-includes">Aggregate includes</h3>
<p>This is the biggest new feature. You can now include aggregate values for related models using <code>AllowedInclude::min()</code>, <code>AllowedInclude::max()</code>, <code>AllowedInclude::sum()</code>, and <code>AllowedInclude::avg()</code>. Under the hood, these map to Laravel's <code>withMin()</code>, <code>withMax()</code>, <code>withSum()</code> and <code>withAvg()</code> methods.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\QueryBuilder\AllowedInclude</span>;

<span class="hl-variable">$users</span> = <span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">allowedIncludes</span>(
        <span class="hl-value">'posts'</span>,
        <span class="hl-type">AllowedInclude</span>::<span class="hl-property">count</span>(<span class="hl-value">'postsCount'</span>),
        <span class="hl-type">AllowedInclude</span>::<span class="hl-property">sum</span>(<span class="hl-value">'postsViewsSum'</span>, <span class="hl-value">'posts'</span>, <span class="hl-value">'views'</span>),
        <span class="hl-type">AllowedInclude</span>::<span class="hl-property">avg</span>(<span class="hl-value">'postsViewsAvg'</span>, <span class="hl-value">'posts'</span>, <span class="hl-value">'views'</span>),
    )
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>A request to <code>/users?include=posts,postsCount,postsViewsSum</code> now returns users with their posts, the post count, and the total views across all posts.</p>
<p>You can constrain these aggregates too. For example, to only count published posts:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\QueryBuilder\AllowedInclude</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Database\Eloquent\Builder</span>;

<span class="hl-variable">$users</span> = <span class="hl-type">QueryBuilder</span>::<span class="hl-keyword">for</span>(<span class="hl-type">User</span>::<span class="hl-keyword">class</span>)
    -&gt;<span class="hl-property">allowedIncludes</span>(
        <span class="hl-type">AllowedInclude</span>::<span class="hl-property">count</span>(
            <span class="hl-value">'publishedPostsCount'</span>,
            <span class="hl-value">'posts'</span>,
            <span class="hl-keyword">fn</span> (<span class="hl-injection"><span class="hl-type">Builder</span> $query</span>) =&gt; <span class="hl-variable">$query</span>-&gt;<span class="hl-property">where</span>(<span class="hl-value">'published'</span>, <span class="hl-keyword">true</span>)
        ),
        <span class="hl-type">AllowedInclude</span>::<span class="hl-property">sum</span>(
            <span class="hl-value">'publishedPostsViewsSum'</span>,
            <span class="hl-value">'posts'</span>,
            <span class="hl-value">'views'</span>,
            <span class="hl-property">constraint</span>: <span class="hl-keyword">fn</span> (<span class="hl-injection"><span class="hl-type">Builder</span> $query</span>) =&gt; <span class="hl-variable">$query</span>-&gt;<span class="hl-property">where</span>(<span class="hl-value">'published'</span>, <span class="hl-keyword">true</span>)
        ),
    )
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>All four aggregate types support these constraint closures, making it possible to build endpoints that return computed data alongside your models without writing custom query logic.</p>
<h2 id="a-perfect-match-for-laravels-jsonapi-resources">A perfect match for Laravel's JSON:API resources</h2>
<p>Laravel 13 is adding built-in support for <a href="https://laravel.com/docs/master/eloquent-resources#jsonapi-resources">JSON:API resources</a>. These new <code>JsonApiResource</code> classes handle the serialization side: they produce responses compliant with the JSON:API specification.</p>
<p>You create one by adding the <code>--json-api</code> flag:</p>
<pre data-lang="txt" class="notranslate">php artisan make:resource PostResource --json-api
</pre>
<p>This generates a resource class where you define attributes and relationships:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Http\Resources\JsonApi\JsonApiResource</span>;

<span class="hl-keyword">class</span> <span class="hl-type">PostResource</span> <span class="hl-keyword">extends</span> <span class="hl-type">JsonApiResource</span>
{
    <span class="hl-keyword">public</span> <span class="hl-property">$attributes</span> = [
        <span class="hl-value">'title'</span>,
        <span class="hl-value">'body'</span>,
        <span class="hl-value">'created_at'</span>,
    ];

    <span class="hl-keyword">public</span> <span class="hl-property">$relationships</span> = [
        <span class="hl-value">'author'</span>,
        <span class="hl-value">'comments'</span>,
    ];
}
</pre>
<p>Return it from your controller, and Laravel produces a fully compliant JSON:API response:</p>
<pre data-lang="json" class="notranslate"><span class="hl-property">{</span>
    <span class="hl-keyword">&quot;data&quot;</span>: <span class="hl-property">{</span>
        <span class="hl-keyword">&quot;id&quot;</span>: <span class="hl-value">&quot;1&quot;</span>,
        <span class="hl-keyword">&quot;type&quot;</span>: <span class="hl-value">&quot;posts&quot;</span>,
        <span class="hl-keyword">&quot;attributes&quot;</span>: <span class="hl-property">{</span>
            <span class="hl-keyword">&quot;title&quot;</span>: <span class="hl-value">&quot;Hello World&quot;</span>,
            <span class="hl-keyword">&quot;body&quot;</span>: <span class="hl-value">&quot;This is my first post.&quot;</span>
        <span class="hl-property">}</span>,
        <span class="hl-keyword">&quot;relationships&quot;</span>: <span class="hl-property">{</span>
            <span class="hl-keyword">&quot;author&quot;</span>: <span class="hl-property">{</span>
                <span class="hl-keyword">&quot;data&quot;</span>: <span class="hl-property">{</span> <span class="hl-keyword">&quot;id&quot;</span>: <span class="hl-value">&quot;1&quot;</span>, <span class="hl-keyword">&quot;type&quot;</span>: <span class="hl-value">&quot;users&quot;</span> <span class="hl-property">}</span>
            <span class="hl-property">}</span>
        <span class="hl-property">}</span>
    <span class="hl-property">}</span>,
    <span class="hl-keyword">&quot;included&quot;</span>: <span class="hl-property">[</span>
        <span class="hl-property">{</span>
            <span class="hl-keyword">&quot;id&quot;</span>: <span class="hl-value">&quot;1&quot;</span>,
            <span class="hl-keyword">&quot;type&quot;</span>: <span class="hl-value">&quot;users&quot;</span>,
            <span class="hl-keyword">&quot;attributes&quot;</span>: <span class="hl-property">{</span> <span class="hl-keyword">&quot;name&quot;</span>: <span class="hl-value">&quot;Taylor Otwell&quot;</span> <span class="hl-property">}</span>
        <span class="hl-property">}</span>
    <span class="hl-property">]</span>
<span class="hl-property">}</span>
</pre>
<p>Clients can request specific fields and includes via query parameters like <code>/api/posts?fields[posts]=title&amp;include=author</code>. Laravel's JSON:API resources handle all of that on the response side.</p>
<p>The <a href="https://laravel.com/docs/master/eloquent-resources#jsonapi-resources">Laravel docs</a> explicitly mention our package as a companion:</p>
<blockquote>
<p>Laravel's JSON:API resources handle the serialization of your responses. If you also need to parse incoming JSON:API query parameters such as filters and sorts, Spatie's Laravel Query Builder is a great companion package.</p>
</blockquote>
<p>So while Laravel's new JSON:API resources take care of the output format, our query builder handles the input side: parsing <code>filter</code>, <code>sort</code>, <code>include</code> and <code>fields</code> parameters from the request and translating them into the right Eloquent queries. Together they give you a full JSON:API implementation with very little boilerplate.</p>
<h2 id="in-closing">In closing</h2>
<p>To upgrade from v6, check the <a href="https://github.com/spatie/laravel-query-builder/blob/main/UPGRADING.md">upgrade guide</a>. The changes are mostly mechanical. Check the guide for the full list.</p>
<p>You can find the full source code and documentation <a href="https://github.com/spatie/laravel-query-builder">on GitHub</a>. We also have extensive <a href="https://spatie.be/docs/laravel-query-builder/v7/introduction">documentation</a> on the Spatie website.</p>
<p>This is one of the many packages we've created at <a href="https://spatie.be/open-source">Spatie</a>. If you want to support our open source work, consider picking up one of our <a href="https://spatie.be/products">paid products</a>.</p>
]]>
            </summary>
                                    <updated>2026-03-16T14:57:04+01:00</updated>
        </entry>
    </feed>
