<?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/php</id>
                                <link href="https://freek.dev/feed/php" rel="self"></link>
                                <title><![CDATA[freek.dev - all PHP blogposts]]></title>
                    
                                <subtitle>All PHP blogposts on freek.dev</subtitle>
                                                    <updated>2026-06-01T14:30:31+02:00</updated>
                        <entry>
            <title><![CDATA[Using property hooks in PHP]]></title>
            <link rel="alternate" href="https://freek.dev/3125-using-property-hooks-in-php" />
            <id>https://freek.dev/3125</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Michael Dyrynda shows how PHP 8.4 property hooks can replace simple computed getter methods with virtual properties. He makes the case for using them when you want a clean, property-based API for derived values.</p>


<a href='https://dyrynda.com.au/blog/using-property-hooks-in-php'>Read more</a>]]>
            </summary>
                                    <updated>2026-06-01T14:30:31+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Validating distinct data in requests]]></title>
            <link rel="alternate" href="https://freek.dev/3124-validating-distinct-data-in-requests" />
            <id>https://freek.dev/3124</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Michael Dyrynda shows how Laravel's <code>distinct</code> validation rule can protect nested request payloads from duplicate reference values before they corrupt relationship mapping. He also highlights the <code>strict</code> and <code>ignore_case</code> options for cases where loose comparison is not enough.</p>


<a href='https://dyrynda.com.au/blog/validating-distinct-request-data'>Read more</a>]]>
            </summary>
                                    <updated>2026-05-31T14:30:37+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[The stack behind There There]]></title>
            <link rel="alternate" href="https://freek.dev/3133-the-stack-behind-there-there" />
            <id>https://freek.dev/3133</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>An inside look at the stack powering There There: Laravel, Inertia, React, TypeScript, Horizon, Reverb, and a bunch of Spatie packages and services. A nice overview of the pragmatic tooling choices behind the product.</p>


<a href='https://there-there.app/blog/the-stack-behind-there-there'>Read more</a>]]>
            </summary>
                                    <updated>2026-05-30T10:13:56+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[An Update on Composer & Packagist Supply Chain Security]]></title>
            <link rel="alternate" href="https://freek.dev/3128-an-update-on-composer-packagist-supply-chain-security" />
            <id>https://freek.dev/3128</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Composer and Packagist share a solid overview of the supply chain security work already in place, what is shipping now, and what is coming next. Worth reading if you maintain PHP packages or care about how the ecosystem is hardening against package compromise.</p>


<a href='https://blog.packagist.com/an-update-on-composer-packagist-supply-chain-security/'>Read more</a>]]>
            </summary>
                                    <updated>2026-05-27T21:09:01+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Using the ADR (Action/Domain/Responder) Pattern in Laravel]]></title>
            <link rel="alternate" href="https://freek.dev/3074-using-the-adr-actiondomainresponder-pattern-in-laravel" />
            <id>https://freek.dev/3074</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Learn what the ADR (Action/Domain/Responder) pattern is and how to apply it in Laravel with a simple, practical example.</p>


<a href='https://wendelladriel.com/blog/using-the-adr-action-domain-responder-pattern-in-laravel'>Read more</a>]]>
            </summary>
                                    <updated>2026-05-12T14:30:29+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Flaky Test Retries in Pest v4.5.0]]></title>
            <link rel="alternate" href="https://freek.dev/3096-flaky-test-retries-in-pest-v450" />
            <id>https://freek.dev/3096</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Pest v4.5.0 adds first-class support for flaky tests with retries and a dedicated CLI filter. The release also brings a useful casing assertion for catching namespace and file path mismatches, plus a coverage option that hides uncovered files.</p>


<a href='https://laravel-news.com/pest-4-5-0'>Read more</a>]]>
            </summary>
                                    <updated>2026-05-10T14:30:29+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Idempotency: What, Why and How]]></title>
            <link rel="alternate" href="https://freek.dev/3113-idempotency-what-why-and-how" />
            <id>https://freek.dev/3113</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 idempotency, from the theory behind safe retries to a practical Laravel implementation using the Laravel Idempotency package.</p>


<a href='https://wendelladriel.com/blog/idempotency-what-why-and-how'>Read more</a>]]>
            </summary>
                                    <updated>2026-05-07T14:33:25+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ Searching multiple columns with one URL parameter in laravel-query-builder]]></title>
            <link rel="alternate" href="https://freek.dev/3118-searching-multiple-columns-with-one-url-parameter-in-laravel-query-builder" />
            <id>https://freek.dev/3118</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.3.0 of <a href="https://spatie.be/docs/laravel-query-builder">laravel-query-builder</a>, which adds a new way to group multiple filters under a single URL parameter. Before getting into the new feature, let me show you how the basics work, so the new bit makes sense in context.</p>
<!--more-->
<h2 id="the-basics">The basics</h2>
<p>Here's a typical setup in a controller:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\QueryBuilder\AllowedFilter</span>;
<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-type">AllowedFilter</span>::<span class="hl-property">partial</span>(<span class="hl-value">'name'</span>),
        <span class="hl-type">AllowedFilter</span>::<span class="hl-property">exact</span>(<span class="hl-value">'status'</span>),
    )
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>With that in place, the package wires up two filters that clients can use through the query string.</p>
<p>A request to <code>/users?filter[name]=john</code> runs:</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> <span class="hl-property">LOWER</span>(name) <span class="hl-keyword">LIKE</span> '<span class="hl-value">%john%</span>'
</pre>
<p>A request to <code>/users?filter[status]=active</code> runs:</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> status = '<span class="hl-value">active</span>'
</pre>
<p>And a request to <code>/users?filter[name]=john&amp;filter[status]=active</code> runs:</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> <span class="hl-property">LOWER</span>(name) <span class="hl-keyword">LIKE</span> '<span class="hl-value">%john%</span>'
  <span class="hl-keyword">AND</span> status = '<span class="hl-value">active</span>'
</pre>
<p>Multiple filters in the URL are joined with <code>AND</code>. That's the default and it's what you want most of the time.</p>
<p>The word &quot;allowed&quot; in <code>allowedFilters</code> is doing real work. If a client tries to filter by something you haven't whitelisted, the package throws an <code>InvalidFilterQuery</code> exception instead of silently ignoring it or, worse, letting it through. So a request to <code>/users?filter[email]=acme.com</code> against the controller above results in:</p>
<pre data-lang="txt" class="notranslate">Spatie\QueryBuilder\Exceptions\InvalidFilterQuery
Requested filter(s) `email` are not allowed.
Allowed filter(s) are `name, status`.
</pre>
<p>The exception has a <code>400 Bad Request</code> status code, so by default Laravel renders it as a clean error response to the client. You don't have to do anything special to get this behavior.</p>
<h2 id="the-new-bit-grouping-filters">The new bit: grouping filters</h2>
<p>Now imagine you want a single search box that matches across multiple columns. The user types &quot;john&quot; and you want to find rows where either the <code>name</code> or the <code>email</code> contains that value.</p>
<p>Before v7.3.0, you'd reach for a custom callback filter:</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-type">AllowedFilter</span>::<span class="hl-property">exact</span>(<span class="hl-value">'status'</span>),
        <span class="hl-type">AllowedFilter</span>::<span class="hl-property">callback</span>(<span class="hl-value">'q'</span>, <span class="hl-keyword">function</span> (<span class="hl-injection">$query, $value</span>) {
            <span class="hl-variable">$query</span>-&gt;<span class="hl-property">where</span>(<span class="hl-value">'name'</span>, <span class="hl-value">'LIKE'</span>, <span class="hl-value">&quot;%{$value}%&quot;</span>)
                -&gt;<span class="hl-property">orWhere</span>(<span class="hl-value">'email'</span>, <span class="hl-value">'LIKE'</span>, <span class="hl-value">&quot;%{$value}%&quot;</span>);
        }),
    )
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>Looks fine at a glance. But a request to <code>/users?filter[status]=active&amp;filter[q]=john</code> runs:</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> status = '<span class="hl-value">active</span>'
  <span class="hl-keyword">AND</span> name <span class="hl-keyword">LIKE</span> '<span class="hl-value">%john%</span>'
   <span class="hl-keyword">OR</span> email <span class="hl-keyword">LIKE</span> '<span class="hl-value">%john%</span>'
</pre>
<p>Spot the bug. The <code>OR</code> binds looser than the <code>AND</code>, so this matches every row whose <code>email</code> contains &quot;john&quot;, including inactive users. To fix it, you have to wrap the callback's <code>where</code>s in their own nested closure. It's an easy thing to forget, and it silently returns wrong data when you do.</p>
<p>In v7.3.0, this becomes a one-liner. Here's how you register a group filter:</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-type">AllowedFilter</span>::<span class="hl-property">groupOr</span>(<span class="hl-value">'q'</span>, [
            <span class="hl-type">AllowedFilter</span>::<span class="hl-property">partial</span>(<span class="hl-value">'name'</span>),
            <span class="hl-type">AllowedFilter</span>::<span class="hl-property">partial</span>(<span class="hl-value">'email'</span>),
        ]),
    )
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>A request to <code>/users?filter[q]=john</code> now runs:</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> (<span class="hl-property">LOWER</span>(name) <span class="hl-keyword">LIKE</span> '<span class="hl-value">%john%</span>' <span class="hl-keyword">OR</span> <span class="hl-property">LOWER</span>(email) <span class="hl-keyword">LIKE</span> '<span class="hl-value">%john%</span>')
</pre>
<p>The value <code>john</code> is passed to every member of the group, and the conjunction joins them. There's also <code>AllowedFilter::groupAnd()</code> if you want all members to match instead.</p>
<h2 id="combining-group-filters-with-regular-filters">Combining group filters with regular filters</h2>
<p>Groups compose cleanly with the rest of your filters. Here's a setup that has both a top-level <code>status</code> filter and a group:</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-type">AllowedFilter</span>::<span class="hl-property">exact</span>(<span class="hl-value">'status'</span>),
        <span class="hl-type">AllowedFilter</span>::<span class="hl-property">groupOr</span>(<span class="hl-value">'q'</span>, [
            <span class="hl-type">AllowedFilter</span>::<span class="hl-property">partial</span>(<span class="hl-value">'name'</span>),
            <span class="hl-type">AllowedFilter</span>::<span class="hl-property">partial</span>(<span class="hl-value">'email'</span>),
        ]),
    )
    -&gt;<span class="hl-property">get</span>();
</pre>
<p>A request to <code>/users?filter[status]=active&amp;filter[q]=john</code> runs:</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> status = '<span class="hl-value">active</span>'
  <span class="hl-keyword">AND</span> (<span class="hl-property">LOWER</span>(name) <span class="hl-keyword">LIKE</span> '<span class="hl-value">%john%</span>' <span class="hl-keyword">OR</span> <span class="hl-property">LOWER</span>(email) <span class="hl-keyword">LIKE</span> '<span class="hl-value">%john%</span>')
</pre>
<p>The whole group is wrapped in its own parentheses, so the <code>OR</code> can never leak into the <code>status</code> clause. That's the bug pattern that used to bite people who rolled their own callback filter for cross-column search.</p>
<p>You can also register multiple independent groups. Each one becomes its own wrapped scope, and they're joined with <code>AND</code> between them. The members can be any <code>AllowedFilter</code> type, so you can mix partial, exact, scope, and other filters in the same group.</p>
<h2 id="in-closing">In closing</h2>
<p>The upgrade is fully non-breaking. All existing filter behavior is unchanged. The new feature is documented in the <a href="https://spatie.be/docs/laravel-query-builder/v7/features/filtering">filtering docs</a>, and the <a href="https://github.com/spatie/laravel-query-builder/pull/1060">pull request</a> by Birtan Taşkın has the full reasoning behind the design.</p>
<p>This is one of the many 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 our <a href="https://spatie.be/products">paid products</a>.</p>
]]>
            </summary>
                                    <updated>2026-05-04T10:09:06+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[RBAC in Laravel: A Practical Deep Dive]]></title>
            <link rel="alternate" href="https://freek.dev/3101-rbac-in-laravel-a-practical-deep-dive" />
            <id>https://freek.dev/3101</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 Role-Based Access Control, from the theory behind roles and permissions to a practical, team-aware Laravel implementation without external packages.</p>


<a href='https://wendelladriel.com/blog/rbac-in-laravel-a-practical-deep-dive'>Read more</a>]]>
            </summary>
                                    <updated>2026-05-01T14:40:29+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ Announcing laravel-sluggable v4 with self-healing URLs]]></title>
            <link rel="alternate" href="https://freek.dev/3112-announcing-laravel-sluggable-v4-with-self-healing-urls" />
            <id>https://freek.dev/3112</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>The <a href="https://github.com/spatie/laravel-sluggable"><code>spatie/laravel-sluggable</code></a> package has been around for close to a decade. A slug is the readable part of a URL that identifies a record, like <code>announcing-laravel-sluggable-v4-with-self-healing-urls</code> in this post's URL. The package generates one for any Eloquent model when you save it, derived from a title or another text field, and most of the time you don't have to think about it.</p>
<p>We just released v4, which adds a few things worth talking about. Let me walk you through them.</p>
<!--more-->
<h2 id="generating-slugs-with-an-attribute">Generating slugs with an attribute</h2>
<p>For most models, slug generation is mechanical. Pick a source field, pick a destination field, done. In v4, you can express that with an attribute on the model.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\Attributes\Sluggable</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Database\Eloquent\Model</span>;

<span class="hl-injection"><span class="hl-attribute">#[<span class="hl-type">Sluggable</span>(<span class="hl-property">from</span>: <span class="hl-value">'title'</span>, <span class="hl-property">to</span>: <span class="hl-value">'slug'</span>)]</span></span>
<span class="hl-keyword">class</span> <span class="hl-type">Post</span> <span class="hl-keyword">extends</span> <span class="hl-type">Model</span>
{
}
</pre>
<p>That's it. No trait, no <code>getSlugOptions()</code> method. Save a <code>Post</code> and the package fills in the slug:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$post</span> = <span class="hl-type">Post</span>::<span class="hl-property">create</span>([<span class="hl-value">'title'</span> =&gt; <span class="hl-value">'ActiveRecord is awesome'</span>]);

<span class="hl-variable">$post</span>-&gt;<span class="hl-property">slug</span>; <span class="hl-comment">// &quot;activerecord-is-awesome&quot;</span>
</pre>
<h3 id="self-healing-urls">Self-healing URLs</h3>
<p>Picture this. You publish a new post titled &quot;Anouncing v4 of laravel-sluggable&quot;. The slug becomes <code>anouncing-v4-of-laravel-sluggable</code>, you share the link, a few people bookmark it. Five minutes later you notice the missing <code>n</code> and fix the title. The slug regenerates to <code>announcing-v4-of-laravel-sluggable</code>, and now everyone who clicked the first version hits a 404.</p>
<p>The usual answer is a redirects table: store the old slug, forward it to the new one. It works, but every rename adds a row, and after a while it's another moving piece to maintain.</p>
<p>Self-healing URLs solve this differently. The route key combines the slug with the primary key, so the slug is decorative and the id is what actually identifies the record.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\Attributes\Sluggable</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\HasSlug</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Database\Eloquent\Model</span>;

<span class="hl-injection"><span class="hl-attribute">#[<span class="hl-type">Sluggable</span>(<span class="hl-property">from</span>: <span class="hl-value">'title'</span>, <span class="hl-property">to</span>: <span class="hl-value">'slug'</span>, <span class="hl-property">selfHealing</span>: <span class="hl-keyword">true</span>)]</span></span>
<span class="hl-keyword">class</span> <span class="hl-type">Post</span> <span class="hl-keyword">extends</span> <span class="hl-type">Model</span>
{
    <span class="hl-keyword">use</span> <span class="hl-type">HasSlug</span>;
}
</pre>
<p>If your post has slug <code>hello-world</code> and id <code>5</code>, the URL becomes <code>/posts/hello-world-5</code>. Rename the post to <code>Greetings, World</code> and the canonical URL becomes <code>/posts/greetings-world-5</code>. Old links keep working because the package can still find the record by id, and it sends a permanent redirect to the new canonical URL.</p>
<p>The redirect status code is <code>308 Permanent Redirect</code>. In v3 it was <code>301</code>. The reason for the change is that <code>301</code> is allowed to silently downgrade <code>PUT</code>, <code>PATCH</code>, and <code>DELETE</code> to <code>GET</code> when followed, which gets you a <code>405 Method Not Allowed</code> if the new URL doesn't accept <code>GET</code>. <code>308</code> keeps the method intact.</p>
<h3 id="overridable-actions">Overridable actions</h3>
<p>Slug generation and the self-healing URL format are now expressed as three actions: one for generating the slug, one for building the self-healing route key, and one for parsing it back out. All three can be swapped through <code>config/sluggable.php</code>. The example below customizes the two self-healing actions; the slug generator stays on its default behavior.</p>
<p>The use case I had in mind was self-healing URLs that put the id first. Say you want <code>/posts/5-hello-world</code> instead of <code>/posts/hello-world-5</code>. Extend the default builder action and put the identifier in front:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">namespace</span> <span class="hl-type">App\Sluggable</span>;

<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\Actions\BuildSelfHealingRouteKeyAction</span>;

<span class="hl-keyword">class</span> <span class="hl-type">IdFirstBuildAction</span> <span class="hl-keyword">extends</span> <span class="hl-type">BuildSelfHealingRouteKeyAction</span>
{
    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">execute</span>(<span class="hl-injection"><span class="hl-type">string</span> $slug, <span class="hl-type">int|string</span> $identifier, <span class="hl-type">string</span> $separator</span>): <span class="hl-type">string</span>
    {
        <span class="hl-keyword">if</span> (<span class="hl-variable">$slug</span> === <span class="hl-value">''</span>) {
            <span class="hl-keyword">return</span> (<span class="hl-type">string</span>) <span class="hl-variable">$identifier</span>;
        }

        <span class="hl-keyword">return</span> <span class="hl-value">&quot;{$identifier}{$separator}{$slug}&quot;</span>;
    }
}
</pre>
<p>The extractor needs the inverse logic. The default action looks for the last separator, because slugs can contain the separator themselves, so swap it for one that reads the identifier from the front:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">namespace</span> <span class="hl-type">App\Sluggable</span>;

<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Support\Str</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\Actions\ExtractIdentifierFromSelfHealingRouteKeyAction</span>;

<span class="hl-keyword">class</span> <span class="hl-type">IdFirstExtractAction</span> <span class="hl-keyword">extends</span> <span class="hl-type">ExtractIdentifierFromSelfHealingRouteKeyAction</span>
{
    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">execute</span>(<span class="hl-injection"><span class="hl-type">string</span> $value, <span class="hl-type">string</span> $separator</span>): <span class="hl-type">array</span>
    {
        <span class="hl-variable">$identifier</span> = <span class="hl-type">Str</span>::<span class="hl-property">before</span>(<span class="hl-variable">$value</span>, <span class="hl-variable">$separator</span>);

        <span class="hl-keyword">if</span> (<span class="hl-variable">$identifier</span> === <span class="hl-variable">$value</span> <span class="hl-operator">||</span> ! <span class="hl-property">ctype_digit</span>(<span class="hl-variable">$identifier</span>)) {
            <span class="hl-keyword">return</span> [<span class="hl-value">'slug'</span> =&gt; <span class="hl-variable">$value</span>, <span class="hl-value">'identifier'</span> =&gt; null];
        }

        <span class="hl-keyword">return</span> [
            <span class="hl-value">'slug'</span> =&gt; <span class="hl-type">Str</span>::<span class="hl-property">after</span>(<span class="hl-variable">$value</span>, <span class="hl-variable">$separator</span>),
            <span class="hl-value">'identifier'</span> =&gt; <span class="hl-variable">$identifier</span>,
        ];
    }
}
</pre>
<p><code>Str::before</code> returns the original string when the separator isn't present, which is how the no-separator case is detected. The <code>ctype_digit</code> check rejects values where the prefix isn't numeric, so models with non-numeric primary keys like ULIDs need to swap that for a regex.</p>
<p>Wire both into the config and you're done:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// config/sluggable.php</span>
<span class="hl-value">'build_self_healing_route_key'</span> =&gt; <span class="hl-type">App\Sluggable\IdFirstBuildAction</span>::<span class="hl-keyword">class</span>,
<span class="hl-value">'extract_identifier_from_self_healing_route_key'</span> =&gt; <span class="hl-type">App\Sluggable\IdFirstExtractAction</span>::<span class="hl-keyword">class</span>,
</pre>
<p>The full reference, including a simpler uppercase variant and a custom slug generator, lives in the <a href="https://spatie.be/docs/laravel-sluggable/v4/advanced-usage/overriding-actions">overriding actions docs</a>.</p>
<h3 id="a-bundled-boost-skill">A bundled Boost skill</h3>
<p><a href="https://github.com/laravel/boost">Laravel Boost</a> is an MCP server from the Laravel team that exposes your application's installed packages and versions to your AI assistant, so prompts produce code that fits your stack instead of generic Laravel snippets. Packages can bundle their own Boost skills, and Boost discovers them automatically.</p>
<p>This package ships with a <a href="https://github.com/spatie/laravel-sluggable/blob/main/resources/boost/skills/sluggable-development/SKILL.md">sluggable-development skill</a> that walks your assistant through adding a sluggable model: picking the source field, deciding whether you need self-healing URLs, generating the migration, and dropping the attribute or trait on the model. If you have Boost installed, this is the easiest way to add a new sluggable model to your project.</p>
<h2 id="in-closing">In closing</h2>
<p>You can find the <a href="https://github.com/spatie/laravel-sluggable">package on GitHub</a> and the full <a href="https://spatie.be/docs/laravel-sluggable/v4">documentation on our docs site</a>. Coming from v3? The <a href="https://github.com/spatie/laravel-sluggable/blob/main/UPGRADING.md">upgrade guide</a> covers the breaking changes (mostly minimum versions and a <code>callable</code> to <code>Closure</code> migration).</p>
<p>This is one of <a href="https://spatie.be/open-source">many packages we've built at 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-04-29T16:36:36+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Validating Array Inputs in Laravel Without the N+1]]></title>
            <link rel="alternate" href="https://freek.dev/3070-validating-array-inputs-in-laravel-without-the-n1" />
            <id>https://freek.dev/3070</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Validate nested array inputs in Laravel form requests without the N+1. Prefetch lookup data in prepareForValidation and check items in memory.</p>


<a href='https://daryllegion.com/validating-array-inputs-in-laravel-without-the-n-plus-1'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-16T14:22:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Why use static closures?]]></title>
            <link rel="alternate" href="https://freek.dev/3066-why-use-static-closures" />
            <id>https://freek.dev/3066</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A clear walkthrough of how PHP closures implicitly capture $this, even when they don't use it, and how that can prevent objects from being garbage collected. Also covers what PHP 8.6 will change with automatic static inference.</p>


<a href='https://f2r.github.io/en/static-closures'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-14T12:30:26+02: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[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[★ 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>
            <entry>
            <title><![CDATA[★ How to easily access private properties and methods in PHP]]></title>
            <link rel="alternate" href="https://freek.dev/3048-how-to-easily-access-private-properties-and-methods-in-php" />
            <id>https://freek.dev/3048</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Sometimes you need to access a private property or method on an object that isn't yours. Maybe you're writing a test and need to assert some internal state. Maybe you're building a package that needs to reach into another object's internals. Whatever the reason, PHP's visibility rules are standing in your way.</p>
<p>Our <a href="https://github.com/spatie/invade">spatie/invade</a> package provides a tiny <code>invade</code> function that lets you read, write, and call private members on any object.</p>
<p>You probably shouldn't reach for this package often. It's most useful in tests or when you're building a package that needs to integrate deeply with objects you don't control.</p>
<p>Let me walk you through how it works.</p>
<!--more-->
<h2 id="using-invade">Using invade</h2>
<p>Imagine you have a class with private members:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">class</span> <span class="hl-type">MyClass</span>
{
    <span class="hl-keyword">private</span> <span class="hl-type">string</span> <span class="hl-property">$privateProperty</span> = <span class="hl-value">'private value'</span>;

    <span class="hl-keyword">private</span> <span class="hl-keyword">function</span> <span class="hl-property">privateMethod</span>(): <span class="hl-type">string</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-value">'private return value'</span>;
    }
}
</pre>
<p>If you try to access that private property from outside the class, PHP will stop you:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$myClass</span> = <span class="hl-keyword">new</span> <span class="hl-type">MyClass</span>();

<span class="hl-variable">$myClass</span>-&gt;<span class="hl-property">privateProperty</span>;
<span class="hl-comment">// Error: Cannot access private property MyClass::$privateProperty</span>
</pre>
<p>With <code>invade</code>, you can get around that. Install the package via composer:</p>
<pre data-lang="txt" class="notranslate">composer require spatie/invade
</pre>
<p>Now you can read that private property:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// returns 'private value'</span>
<span class="hl-property">invade</span>(<span class="hl-variable">$myClass</span>)-&gt;<span class="hl-property">privateProperty</span>;
</pre>
<p>You can set it too:</p>
<pre data-lang="php" class="notranslate"><span class="hl-property">invade</span>(<span class="hl-variable">$myClass</span>)-&gt;<span class="hl-property">privateProperty</span> = <span class="hl-value">'changed value'</span>;

<span class="hl-comment">// returns 'changed value'</span>
<span class="hl-property">invade</span>(<span class="hl-variable">$myClass</span>)-&gt;<span class="hl-property">privateProperty</span>;
</pre>
<p>And you can call private methods:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// returns 'private return value'</span>
<span class="hl-property">invade</span>(<span class="hl-variable">$myClass</span>)-&gt;<span class="hl-property">privateMethod</span>();
</pre>
<p>The API is clean and reads well. But the interesting part is what happens under the hood. Before we look at the package code, there's a PHP rule you need to know about first.</p>
<h2 id="how-it-works-under-the-hood">How it works under the hood</h2>
<p>Let me walk you through how the package works internally. We'll first look at the old approach using reflection, and then the current solution that uses closure binding.</p>
<h3 id="the-first-version-reflection">The first version: reflection</h3>
<p>In v1 of the package, we used PHP's Reflection API to access private members. Here's what the <code>Invader</code> class <a href="https://github.com/spatie/invade/blob/1.1.1/src/Invader.php">looked like</a>:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">class</span> <span class="hl-type">Invader</span>
{
    <span class="hl-keyword">public</span> <span class="hl-type">object</span> <span class="hl-property">$obj</span>;
    <span class="hl-keyword">public</span> <span class="hl-type">ReflectionClass</span> <span class="hl-property">$reflected</span>;

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__construct</span>(<span class="hl-injection"><span class="hl-type">object</span> $obj</span>)
    {
        <span class="hl-variable">$this</span>-&gt;<span class="hl-property">obj</span> = <span class="hl-variable">$obj</span>;
        <span class="hl-variable">$this</span>-&gt;<span class="hl-property">reflected</span> = <span class="hl-keyword">new</span> <span class="hl-type">ReflectionClass</span>(<span class="hl-variable">$obj</span>);
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__get</span>(<span class="hl-injection"><span class="hl-type">string</span> $name</span>): <span class="hl-type">mixed</span>
    {
        <span class="hl-variable">$property</span> = <span class="hl-variable">$this</span>-&gt;<span class="hl-property">reflected</span>-&gt;<span class="hl-property">getProperty</span>(<span class="hl-variable">$name</span>);
        <span class="hl-variable">$property</span>-&gt;<span class="hl-property">setAccessible</span>(<span class="hl-keyword">true</span>);

        <span class="hl-keyword">return</span> <span class="hl-variable">$property</span>-&gt;<span class="hl-property">getValue</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">obj</span>);
    }
}
</pre>
<p>When you create an <code>Invader</code>, it wraps your object and creates a <code>ReflectionClass</code> for it. When you try to access a property like <code>invade($myClass)-&gt;privateProperty</code>, PHP triggers the <code>__get</code> magic method. It uses the reflection instance to find the property by name, calls <code>setAccessible(true)</code> on it, and then reads the value from the original object. The <code>setAccessible(true)</code> call tells PHP to skip the visibility check for that reflected property. Without it, trying to read a private property through reflection would throw an error, just like accessing it directly.</p>
<p>This worked fine, but it required creating a <code>ReflectionClass</code> instance and calling <code>setAccessible(true)</code> on every property or method you wanted to access. In v2, we replaced all of this with a much simpler approach using closures. To understand how, we first need to look at a lesser-known PHP visibility rule.</p>
<h3 id="private-visibility-is-scoped-to-the-class">Private visibility is scoped to the class</h3>
<p>In PHP, <code>private</code> visibility is scoped to the class, not to a specific object instance. Any code running inside a class can access the private properties and methods of any instance of that class.</p>
<p>Here's a concrete example:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">class</span> <span class="hl-type">Wallet</span>
{
    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__construct</span>(<span class="hl-injection">
        <span class="hl-keyword">private</span> <span class="hl-type">int</span> <span class="hl-property">$balance</span>
    </span>) {
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">hasMoreThan</span>(<span class="hl-injection"><span class="hl-type">Wallet</span> $other</span>): <span class="hl-type">bool</span>
    {
        <span class="hl-comment">// This works: we can read $other's private $balance</span>
        <span class="hl-comment">// because we're inside the Wallet class scope</span>
        <span class="hl-keyword">return</span> <span class="hl-variable">$this</span>-&gt;<span class="hl-property">balance</span> &gt; <span class="hl-variable">$other</span>-&gt;<span class="hl-property">balance</span>;
    }
}

<span class="hl-variable">$mine</span> = <span class="hl-keyword">new</span> <span class="hl-type">Wallet</span>(100);
<span class="hl-variable">$yours</span> = <span class="hl-keyword">new</span> <span class="hl-type">Wallet</span>(50);

<span class="hl-comment">// returns true</span>
<span class="hl-variable">$mine</span>-&gt;<span class="hl-property">hasMoreThan</span>(<span class="hl-variable">$yours</span>);
</pre>
<p>Notice how <code>hasMoreThan</code> reads <code>$other-&gt;balance</code> directly, even though <code>$balance</code> is private. This compiles and runs without errors because the code is running inside the <code>Wallet</code> class. PHP doesn't care which instance the property belongs to. As long as you're in the right class scope, all private members of all instances of that class are accessible.</p>
<p>This is the foundation that makes v2 of the <code>invade</code> package work. If you can get your code to run inside the scope of the target class, you get access to its private members. PHP closures give us a way to do exactly that.</p>
<h3 id="how-closures-can-change-their-scope">How closures can change their scope</h3>
<p>PHP closures carry the scope of the class they were defined in. But the <code>Closure::call()</code> method lets you change that. It temporarily rebinds <code>$this</code> inside the closure to a different object, and it also changes the scope to the class of that object.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$readBalance</span> = <span class="hl-keyword">fn</span> () =&gt; <span class="hl-variable">$this</span>-&gt;<span class="hl-property">balance</span>;

<span class="hl-variable">$wallet</span> = <span class="hl-keyword">new</span> <span class="hl-type">Wallet</span>(100);
<span class="hl-comment">// returns 100</span>
<span class="hl-variable">$readBalance</span>-&gt;<span class="hl-property">call</span>(<span class="hl-variable">$wallet</span>);
</pre>
<p>Even though <code>$balance</code> is private, this works. The <code>-&gt;call($wallet)</code> method binds the closure to the <code>$wallet</code> object and puts it in the <code>Wallet</code> class scope. When PHP evaluates <code>$this-&gt;balance</code>, it sees that the code is running in the scope of <code>Wallet</code>, so it allows the access.</p>
<p>This is the entire trick that <code>invade</code> v2 is built on. Now let's look at the actual code.</p>
<h3 id="the-current-invader-class">The current Invader class</h3>
<p>When you call <code>invade($object)</code>, it returns an <code>Invader</code> instance that wraps your object. The <a href="https://github.com/spatie/invade/blob/main/src/Invader.php">current version</a> of the <code>Invader</code> class is surprisingly small:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">class</span> <span class="hl-type">Invader</span>
{
    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__construct</span>(<span class="hl-injection">
        <span class="hl-keyword">public</span> <span class="hl-type">object</span> <span class="hl-property">$obj</span>
    </span>) {
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__get</span>(<span class="hl-injection"><span class="hl-type">string</span> $name</span>): <span class="hl-type">mixed</span>
    {
        <span class="hl-keyword">return</span> (<span class="hl-keyword">fn</span> () =&gt; <span class="hl-variable">$this</span>-&gt;{<span class="hl-variable">$name</span>})-&gt;<span class="hl-property">call</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">obj</span>);
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__set</span>(<span class="hl-injection"><span class="hl-type">string</span> $name, <span class="hl-type">mixed</span> $value</span>): <span class="hl-type">void</span>
    {
        (<span class="hl-keyword">fn</span> () =&gt; <span class="hl-variable">$this</span>-&gt;{<span class="hl-variable">$name</span>} = <span class="hl-variable">$value</span>)-&gt;<span class="hl-property">call</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">obj</span>);
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__call</span>(<span class="hl-injection"><span class="hl-type">string</span> $name, <span class="hl-type">array</span> $params = []</span>): <span class="hl-type">mixed</span>
    {
        <span class="hl-keyword">return</span> (<span class="hl-keyword">fn</span> () =&gt; <span class="hl-variable">$this</span>-&gt;{<span class="hl-variable">$name</span>}(...<span class="hl-variable">$params</span>))-&gt;<span class="hl-property">call</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">obj</span>);
    }
}
</pre>
<p>That's the entire class. No reflection, no complex tricks. Just PHP magic methods and closures.</p>
<p>When you write <code>invade($myClass)-&gt;privateProperty</code>, the <a href="https://github.com/spatie/invade/blob/main/src/functions.php"><code>invade</code> function</a> creates a new <code>Invader</code> instance. PHP can't find <code>privateProperty</code> on the <code>Invader</code> class, so it triggers <code>__get('privateProperty')</code>. The <code>__get</code> method creates a short closure <code>fn () =&gt; $this-&gt;{$name}</code> and calls it with <code>-&gt;call($this-&gt;obj)</code>. As we just learned, this binds <code>$this</code> inside the closure to your original object and puts the closure in that object's class scope. PHP then evaluates <code>$this-&gt;privateProperty</code> inside the scope of <code>MyClass</code>, and the private access is allowed.</p>
<p>The <code>__set</code> method uses the same pattern, but assigns a value instead of reading one:</p>
<pre data-lang="php" class="notranslate">(<span class="hl-keyword">fn</span> () =&gt; <span class="hl-variable">$this</span>-&gt;{<span class="hl-variable">$name</span>} = <span class="hl-variable">$value</span>)-&gt;<span class="hl-property">call</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">obj</span>);
</pre>
<p>The <code>$value</code> variable is captured from the enclosing scope of the <code>__set</code> method, so it's available inside the closure.</p>
<p>For calling private methods, <code>__call</code> follows the same approach:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">return</span> (<span class="hl-keyword">fn</span> () =&gt; <span class="hl-variable">$this</span>-&gt;{<span class="hl-variable">$name</span>}(...<span class="hl-variable">$params</span>))-&gt;<span class="hl-property">call</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">obj</span>);
</pre>
<p>The closure calls the method by name, spreading the parameters. Since <code>-&gt;call()</code> binds the closure to the target object, PHP sees this as a call from within the class itself, and the private method becomes accessible.</p>
<h2 id="in-closing">In closing</h2>
<p>The <code>invade</code> package is a fun example of how PHP closures and scope binding can bypass visibility restrictions in a clean way. It's a small trick, but understanding why it works teaches you something interesting about how PHP handles class scope and closure binding.</p>
<p>The original idea for the <code>invade</code> function came from <a href="https://twitter.com/calebporzio">Caleb Porzio</a>, who first introduced it as a helper in Livewire to replace a more verbose <code>ObjectPrybar</code> class. We liked the concept so much that we turned it into its own package.</p>
<p>Just remember: use it sparingly. It works great in tests or when you're building a package that needs deep integration with objects you don't control. In your regular project code, you probably don't need invade.</p>
<p>You can find the package <a href="https://github.com/spatie/invade">on GitHub</a>. 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 <a href="https://spatie.be/open-source/support-us">one of our paid products</a>.</p>
]]>
            </summary>
                                    <updated>2026-03-12T11:39:39+01:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Quick tips: Sending Laravel output to Ray automatically]]></title>
            <link rel="alternate" href="https://freek.dev/3011-quick-tips-sending-laravel-output-to-ray-automatically" />
            <id>https://freek.dev/3011</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A handy overview of the Ray configuration options in Laravel. You can automatically send duplicate queries, slow queries, exceptions, and dump output straight to Ray without adding any ray() calls to your code.</p>


<a href='https://myray.app/blog/quick-tips-sending-laravel-output-to-ray-automatically'>Read more</a>]]>
            </summary>
                                    <updated>2026-03-11T14:30:27+01:00</updated>
        </entry>
    </feed>
