<?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-09-18T17:39:46+02:00</updated>
                        <entry>
            <title><![CDATA[★ Detecting spam and auto-replies with Jev and the Laravel AI SDK]]></title>
            <link rel="alternate" href="https://freek.dev/3194-detecting-spam-and-auto-replies-with-jev-and-the-laravel-ai-sdk" />
            <id>https://freek.dev/3194</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Yesterday <a href="https://x.com/taylorotwell/status/2100700952923713641">Taylor announced</a> that Jev support landed in the 1.x branch of the Laravel AI SDK. We started using it that same day for spam detection in <a href="https://there-there.app">There There</a>. Let's take a look at what Jev is and how you can use it.</p>
<!--more-->
<h2 id="how-jev-is-different-from-an-llm">How Jev is different from an LLM</h2>
<p><a href="https://docs.typesafe.ai">Jev</a> is made by TypeSafe. An LLM generates text: you ask it something, it writes an answer, and if you want structured data back you have to ask for it and hope. Jev doesn't generate anything. You give it some state and a question, and it gives you back a number.</p>
<p>TypeSafe calls these System One models. They read natural language like an LLM does, but instead of writing a reply they pick between answers that you define up front. The probabilities are calibrated, which means they're trained against real outcomes, so across a batch of answers a 0.9 should be right about nine times out of ten.</p>
<p>In practice that means no prose to parse, and no prompt asking the model to please respond with valid JSON.</p>
<p>The Syntax folks made a video explaining it:</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/QbYBRjOaGOo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
<h2 id="noul-choice-and-score">Noul, Choice and Score</h2>
<p>You define what the answers can be using one of three question types.</p>
<p>A Noul is a yes or no question. The answer is a single number: the probability that the answer is yes. Here's how you ask one:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Laravel\Ai\Classification</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Laravel\Ai\Classification\Boolean</span>;

<span class="hl-variable">$result</span> = <span class="hl-type">Classification</span>::<span class="hl-property">of</span>(<span class="hl-value">'I have asked three times now. Can I please talk to a real person?'</span>)
    -&gt;<span class="hl-property">question</span>(<span class="hl-value">'urgent'</span>, <span class="hl-keyword">new</span> <span class="hl-type">Boolean</span>(<span class="hl-value">'Does this request need an immediate response?'</span>))
    -&gt;<span class="hl-property">classify</span>();

<span class="hl-variable">$result</span>[<span class="hl-value">'urgent'</span>]-&gt;<span class="hl-property">probability</span>;            <span class="hl-comment">// 0.94</span>
<span class="hl-variable">$result</span>[<span class="hl-value">'urgent'</span>]-&gt;<span class="hl-property">isTrue</span>(<span class="hl-property">threshold</span>: 0.8); <span class="hl-comment">// true</span>
</pre>
<p>Notice that you get back <code>0.94</code> instead of <code>true</code>. You decide where the cutoff is, which means you can set a different one per question.</p>
<p>A Choice picks one option out of a set that you name. Here's an example:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Laravel\Ai\Classification\Choice</span>;

<span class="hl-variable">$result</span> = <span class="hl-type">Classification</span>::<span class="hl-property">of</span>(<span class="hl-value">'My card was charged twice for order A-104. Please refund the duplicate.'</span>)
    -&gt;<span class="hl-property">question</span>(<span class="hl-value">'department'</span>, <span class="hl-keyword">new</span> <span class="hl-type">Choice</span>(<span class="hl-value">'Which team should handle this request?'</span>, [
        <span class="hl-value">'billing'</span> =&gt; <span class="hl-value">'Payments, invoices, and refunds'</span>,
        <span class="hl-value">'technical'</span> =&gt; <span class="hl-value">'Bugs, outages, and integrations'</span>,
        <span class="hl-value">'sales'</span> =&gt; <span class="hl-value">'Pricing, plans, and upgrades'</span>,
    ]))
    -&gt;<span class="hl-property">classify</span>();

<span class="hl-variable">$result</span>[<span class="hl-value">'department'</span>]-&gt;<span class="hl-property">choice</span>;                   <span class="hl-comment">// 'billing'</span>
<span class="hl-variable">$result</span>[<span class="hl-value">'department'</span>]-&gt;<span class="hl-property">probabilityOf</span>(<span class="hl-value">'billing'</span>); <span class="hl-comment">// 0.87</span>
<span class="hl-variable">$result</span>[<span class="hl-value">'department'</span>]-&gt;<span class="hl-property">confidence</span>;               <span class="hl-comment">// 0.82</span>
</pre>
<p>Next to the option it picked, you also get a probability for every option, and a confidence score that tells you how concentrated those probabilities were.</p>
<p>A Score rates something against levels that you describe yourself. You could ask how frustrated a customer is, where 0 is calm, 1 is frustrated and 2 is very angry. The answer can land between two levels, so a 1.4 is a perfectly good answer.</p>
<p>The state doesn't have to be a string. When a decision depends on more than one thing, you can pass an array so every part has a name:</p>
<pre data-lang="php" class="notranslate"><span class="hl-type">Classification</span>::<span class="hl-property">of</span>([
    <span class="hl-value">'subject'</span> =&gt; <span class="hl-value">'Duplicate charge'</span>,
    <span class="hl-value">'message'</span> =&gt; <span class="hl-value">'My card was charged twice for order A-104.'</span>,
    <span class="hl-value">'order'</span> =&gt; [<span class="hl-value">'id'</span> =&gt; <span class="hl-value">'A-104'</span>, <span class="hl-value">'charges'</span> =&gt; [49, 49]],
    <span class="hl-value">'refund_policy'</span> =&gt; <span class="hl-value">'Duplicate charges are eligible for a refund.'</span>,
])-&gt;<span class="hl-property">question</span>(<span class="hl-value">'refund_due'</span>, <span class="hl-keyword">new</span> <span class="hl-type">Boolean</span>(<span class="hl-value">'The policy entitles this customer to a refund.'</span>))
    -&gt;<span class="hl-property">classify</span>();
</pre>
<p>That's still one state, even though it holds a message, an order and a policy.</p>
<p>You configure Jev like any other provider in <code>config/ai.php</code>, with a <code>TYPESAFE_API_KEY</code> in your env file.</p>
<h2 id="the-problem-we-wanted-to-solve">The problem we wanted to solve</h2>
<p>You might have noticed that we <a href="https://x.com/freekmurze/status/2100549614248308805">launched There There</a> yesterday as well, our new helpdesk. A lot of the mail that arrives in a helpdesk isn't from a customer. Out-of-office replies, bounces, subscription confirmations, DMARC reports. You don't want those in your inbox, and you don't want to pay an LLM to write a title and a summary for each one.</p>
<p>Some of that mail says what it is in the headers. <code>Auto-Submitted</code>, an empty return path, a sender called mailer-daemon. Checking those costs nothing, so we do that first.</p>
<p>Plenty of mail servers don't set those headers. For those we had a list of subject prefixes: <code>Automatische Antwort</code>, <code>Réponse automatique</code>, <code>Out of office</code>, in fifteen languages. Nine more for bounces. On top of that, rules so that a customer asking a question about auto-replies didn't get treated as one.</p>
<p>Every time we found mail that the list missed, we added another string to it.</p>
<h2 id="replacing-the-list">Replacing the list</h2>
<p>The header checks still run first. Everything they can't answer goes to Jev.</p>
<p>We describe each question once, as a case on an enum that also carries its own threshold. That way adding a fourth question is a single case instead of an edit in four files.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">enum</span> <span class="hl-type">InboundJudgement</span>: <span class="hl-type">string</span>
{
    <span class="hl-keyword">case</span> <span class="hl-property">IsAutoResponse</span> = <span class="hl-value">'is_auto_response'</span>;
    <span class="hl-keyword">case</span> <span class="hl-property">IsBounce</span> = <span class="hl-value">'is_bounce'</span>;
    <span class="hl-keyword">case</span> <span class="hl-property">IsSpam</span> = <span class="hl-value">'is_spam'</span>;

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">question</span>(): <span class="hl-type">Boolean</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-keyword">match</span> (<span class="hl-variable">$this</span>) {
            <span class="hl-type">self</span>::<span class="hl-property">IsAutoResponse</span> =&gt; <span class="hl-keyword">new</span> <span class="hl-type">Boolean</span>(
                <span class="hl-value">'A system sent this mail on its own, rather than a person choosing to write to us.'</span>,
                [
                    <span class="hl-value">'true'</span> =&gt; 'Sent on a trigger with no human involved at send <span class="hl-property">time</span>: out-of-office
                        notices, delivery reports, ticket acknowledgements, subscription
                        confirmations, digests <span class="hl-keyword">and</span> alerts. Wording composed in advance still counts',
                    <span class="hl-value">'false'</span> =&gt; '<span class="hl-property">A</span> person sat down <span class="hl-keyword">and</span> sent this. Still <span class="hl-keyword">false</span> when a contact form <span class="hl-keyword">or</span>
                        chat widget wrapped their words in a template <span class="hl-keyword">and</span> added lines such <span class="hl-keyword">as</span> Name,
                        <span class="hl-property">E</span>-mail <span class="hl-keyword">or</span> Subject',
                ],
            ),
            <span class="hl-type">self</span>::<span class="hl-property">IsSpam</span> =&gt; <span class="hl-keyword">new</span> <span class="hl-type">Boolean</span>(
                'This mail is unsolicited bulk mail, a scam, <span class="hl-keyword">or</span> phishing rather than a genuine
                    message from a customer.',
                [
                    <span class="hl-value">'true'</span> =&gt; 'Cold sales outreach, marketing blasts, scams, phishing, <span class="hl-keyword">or</span> anything
                        the recipient never asked for',
                    <span class="hl-value">'false'</span> =&gt; '<span class="hl-property">A</span> real person writing about the product, their account, <span class="hl-keyword">or</span> their own
                        support request, however brief <span class="hl-keyword">or</span> badly written',
                ],
            ),
            <span class="hl-comment">// ...</span>
        };
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">threshold</span>(): <span class="hl-type">float</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-keyword">match</span> (<span class="hl-variable">$this</span>) {
            <span class="hl-type">self</span>::<span class="hl-property">IsAutoResponse</span> =&gt; 0.75,
            <span class="hl-type">self</span>::<span class="hl-property">IsBounce</span>, <span class="hl-type">self</span>::<span class="hl-property">IsSpam</span> =&gt; 0.9,
        };
    }
}
</pre>
<p>Those <code>true</code> and <code>false</code> descriptions are optional, but I'd recommend writing them. They do more work than the question above them.</p>
<p>All the questions the headers couldn't answer go out in one request. Jev reads the state once and answers them in parallel, and you only pay for input tokens, so asking three questions costs the same as asking one. Here's the action that does it:</p>
<pre data-lang="php" class="notranslate"><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">Message</span> $message, <span class="hl-type">Ticket</span> $ticket, <span class="hl-type">Workspace</span> $workspace</span>): <span class="hl-type">void</span>
{
    <span class="hl-variable">$judgements</span> = <span class="hl-property">array_filter</span>(
        <span class="hl-type">InboundJudgement</span>::<span class="hl-property">cases</span>(),
        <span class="hl-keyword">fn</span> (<span class="hl-injection"><span class="hl-type">InboundJudgement</span> $judgement</span>) =&gt; ! <span class="hl-variable">$judgement</span>-&gt;<span class="hl-property">settledByHeaders</span>(<span class="hl-variable">$message</span>),
    );

    <span class="hl-keyword">if</span> (<span class="hl-variable">$judgements</span> === []) {
        <span class="hl-keyword">return</span>;
    }

    <span class="hl-keyword">try</span> {
        <span class="hl-variable">$response</span> = <span class="hl-type">Classification</span>::<span class="hl-property">of</span>([
            <span class="hl-value">'subject'</span> =&gt; <span class="hl-variable">$ticket</span>-&gt;<span class="hl-property">subject</span>,
            <span class="hl-value">'from_name'</span> =&gt; <span class="hl-variable">$message</span>-&gt;<span class="hl-property">author_name</span>,
            <span class="hl-value">'from_email'</span> =&gt; <span class="hl-variable">$message</span>-&gt;<span class="hl-property">author_email</span> ?? <span class="hl-variable">$ticket</span>-&gt;<span class="hl-property">contact</span>?-&gt;<span class="hl-property">email</span>,
            <span class="hl-value">'message'</span> =&gt; <span class="hl-type">Str</span>::<span class="hl-property">limit</span>(<span class="hl-variable">$message</span>-&gt;<span class="hl-property">body_text</span>, 10_000),
        ])
            -&gt;<span class="hl-property">questions</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">questionsFor</span>(<span class="hl-variable">$judgements</span>))
            -&gt;<span class="hl-property">timeout</span>(10)
            -&gt;<span class="hl-property">classify</span>();

        <span class="hl-variable">$verdicts</span> = <span class="hl-variable">$this</span>-&gt;<span class="hl-property">verdicts</span>(<span class="hl-variable">$judgements</span>, <span class="hl-variable">$response</span>);
    } <span class="hl-keyword">catch</span> (<span class="hl-type">Throwable</span> <span class="hl-variable">$exception</span>) {
        <span class="hl-type">Log</span>::<span class="hl-property">warning</span>(<span class="hl-value">'Could not classify an inbound message.'</span>, [
            <span class="hl-value">'message_id'</span> =&gt; <span class="hl-variable">$message</span>-&gt;<span class="hl-property">id</span>,
            <span class="hl-value">'error'</span> =&gt; <span class="hl-variable">$exception</span>-&gt;<span class="hl-property">getMessage</span>(),
        ]);

        <span class="hl-keyword">return</span>;
    }

    <span class="hl-variable">$message</span>-&gt;<span class="hl-property">updateQuietly</span>([...<span class="hl-variable">$verdicts</span>, <span class="hl-value">'classification'</span> =&gt; <span class="hl-variable">$response</span>-&gt;<span class="hl-property">answers</span>]);
}
</pre>
<p>There are two things in there I'd suggest copying. The whole call sits in a try block that logs the problem and moves on, because a classification is a nice to have and it shouldn't be able to break the mail pipeline it's helping. And we truncate the message, because an inbound mail can be megabytes long and nothing past the first part of it changes what the mail is.</p>
<p>Turning the answers into booleans is where each question's own threshold is applied. We store the raw probabilities next to them, so we can change a threshold later and see what it would have done:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">private</span> <span class="hl-keyword">function</span> <span class="hl-property">verdicts</span>(<span class="hl-injection"><span class="hl-type">array</span> $judgements, <span class="hl-type">ClassificationResponse</span> $response</span>): <span class="hl-type">array</span>
{
    <span class="hl-variable">$verdicts</span> = [];

    <span class="hl-keyword">foreach</span> (<span class="hl-variable">$judgements</span> <span class="hl-keyword">as</span> <span class="hl-variable">$judgement</span>) {
        <span class="hl-variable">$answer</span> = <span class="hl-variable">$response</span>-&gt;<span class="hl-property">answer</span>(<span class="hl-variable">$judgement</span>-&gt;<span class="hl-property">value</span>);

        <span class="hl-variable">$verdicts</span>[<span class="hl-variable">$judgement</span>-&gt;<span class="hl-property">value</span>] = <span class="hl-variable">$answer</span>-&gt;<span class="hl-property">isTrue</span>(<span class="hl-variable">$judgement</span>-&gt;<span class="hl-property">threshold</span>());
    }

    <span class="hl-keyword">return</span> <span class="hl-variable">$verdicts</span>;
}
</pre>
<p>Those verdicts are stored on the message. When a customer builds a workflow in There There with an &quot;Is spam&quot; condition, checking that condition reads a single column and doesn't call Jev at all.</p>
<h2 id="in-closing">In closing</h2>
<p>I like that Jev does one small thing. It gives you a number and leaves the rest of the decisions in your own code, where you can read them and write tests for them.</p>
<p>It's also fast and cheap enough that you don't really have to think about it. Classifying a mail with three questions at once takes 639ms, and we get around 48 per second when we run them in parallel. We didn't spend any time tuning that, so I'm sure you could get more out of it, but for what we're doing it's fast enough. Jev costs $0.042 per million input tokens and output tokens are free, which for us comes down to four hundredths of a cent per mail, or about 36 cents a month.</p>
<p>We have a list of other places where we want to use this in There There, and in our other products. Expect more Jev powered features soon.</p>
<p>If you want to read more, there are the <a href="https://docs.typesafe.ai">TypeSafe docs</a> and the <a href="https://github.com/laravel/ai">Laravel AI SDK</a>. And if you'd like to see the spam detection at work, you can try <a href="https://there-there.app">There There</a>.</p>
]]>
            </summary>
                                    <updated>2026-09-18T17:39:46+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[What's New in PHP 8.6]]></title>
            <link rel="alternate" href="https://freek.dev/3192-whats-new-in-php-86" />
            <id>https://freek.dev/3192</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A concise overview of what's coming in PHP 8.6, including partial function application, clamp(), the new Duration class, and several smaller language and standard library improvements.</p>


<a href='https://laravel-news.com/php-8-6'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-16T14:30:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Using AI as a deterministic translation tool in Laravel]]></title>
            <link rel="alternate" href="https://freek.dev/3186-using-ai-as-a-deterministic-translation-tool-in-laravel" />
            <id>https://freek.dev/3186</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Split a task into reasoning and execution to get deterministic behaviour out of a non-deterministic model.</p>


<a href='https://koomai.net/posts/using-ai-as-a-deterministic-translation-tool-in-laravel/'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-07T14:53:24+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Digital Sovereignty Is Written in PHP]]></title>
            <link rel="alternate" href="https://freek.dev/3185-digital-sovereignty-is-written-in-php" />
            <id>https://freek.dev/3185</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>An interesting article on how PHP is used by the government.</p>


<a href='https://thephp.foundation/blog/2026/09/02/digital-sovereignty-is-written-in-php/'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-04T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Build Your Own AI-Powered Slack Bot with the Laravel AI SDK]]></title>
            <link rel="alternate" href="https://freek.dev/3184-build-your-own-ai-powered-slack-bot-with-the-laravel-ai-sdk" />
            <id>https://freek.dev/3184</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>In this article, we’ll build an AI-powered Slack bot with the Laravel AI SDK. Unlike tools such as Claude Tag, it can switch AI providers, answer questions, execute tasks, and retrieve information from a knowledge base you control.</p>


<a href='https://tighten.com/insights/build-your-own-ai-powered-slack-bot-with-the-laravel-ai-sdk/'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-31T14:59:25+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Immutability in PHP Beyond readonly]]></title>
            <link rel="alternate" href="https://freek.dev/3181-immutability-in-php-beyond-readonly" />
            <id>https://freek.dev/3181</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 immutability in PHP, from readonly properties and interior mutability to immutable value objects, collections, dates, cloning, boundaries, and testing.</p>


<a href='https://wendelladriel.com/blog/immutability-in-php-beyond-readonly'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-17T14:12:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[`exit()` may silently break your parallel tests]]></title>
            <link rel="alternate" href="https://freek.dev/3177-exit-may-silently-break-your-parallel-tests" />
            <id>https://freek.dev/3177</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Michael explains why exit() can make parallel test workers crash without useful diagnostics, and why verbose flags do not help when the process dies outside PHPUnit's control. The fix is simple: throw an exception instead, so the failure is reported normally with a stack trace.</p>


<a href='https://dyrynda.com.au/blog/exit-may-silently-break-your-parallel-tests'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-12T12:30:31+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[sbx: Sandboxed Claude, complete with PHP and tools]]></title>
            <link rel="alternate" href="https://freek.dev/3175-sbx-sandboxed-claude-complete-with-php-and-tools" />
            <id>https://freek.dev/3175</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A practical walkthrough for running Claude Code inside Docker's new sbx sandbox, with a custom PHP setup and extra tools for open source work. Benjamin also explains why sandboxing AI coding agents is the sensible default.</p>


<a href='https://www.beberlei.de/post/sbx-sandboxed-claude-complete-with-php-and-tools'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-10T12:30:04+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[What's new in PHP 8.6]]></title>
            <link rel="alternate" href="https://freek.dev/3174-whats-new-in-php-86" />
            <id>https://freek.dev/3174</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Brent surveys the most notable additions coming in PHP 8.6, including partial function application, the new polling API, readonly property defaults, and several smaller improvements and deprecations.</p>


<a href='https://stitcher.io/blog/new-in-php-86'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-05T14:14:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[PHP version stats: July, 2026]]></title>
            <link rel="alternate" href="https://freek.dev/3171-php-version-stats-july-2026" />
            <id>https://freek.dev/3171</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Brendt shares fresh Packagist-based PHP version stats for July 2026: PHP 8.4 is now the most-used version, while PHP 8.5 reached 16% adoption after six months. He also notes that package minimum requirements still lag behind, with relatively few popular packages requiring a version that still gets active bugfix support.</p>


<a href='https://stitcher.io/blog/php-version-stats-july-2026'>Read more</a>]]>
            </summary>
                                    <updated>2026-07-29T17:23:14+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building an order fulfilment workflow in Laravel]]></title>
            <link rel="alternate" href="https://freek.dev/3170-building-an-order-fulfilment-workflow-in-laravel" />
            <id>https://freek.dev/3170</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Steve King builds a real order fulfilment flow to show how a workflow engine handles long-running processes in Laravel. The post covers steps, signals, retries, branching, human approval, compensation, and the surrounding tooling in one practical example.</p>


<a href='https://www.juststeveking.com/articles/building-an-order-fulfilment-workflow/'>Read more</a>]]>
            </summary>
                                    <updated>2026-07-25T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Preventing SSRF in Laravel: Why Blocking Private IPs Fails]]></title>
            <link rel="alternate" href="https://freek.dev/3167-preventing-ssrf-in-laravel-why-blocking-private-ips-fails" />
            <id>https://freek.dev/3167</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>I found there wasn't any good SSRF prevention libraries for Laravel, so I built one. Very comprehensive tests based on this paper:
https://www.usenix.org/conference/usenixsecurity24/presentation/wessels</p>
<p>https://github.com/securized/laravel-ssrf</p>


<a href='https://securized.dev/laravel-ssrf-package'>Read more</a>]]>
            </summary>
                                    <updated>2026-07-22T15:24:24+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[PHP Attributes: What, Why, How and When]]></title>
            <link rel="alternate" href="https://freek.dev/3166-php-attributes-what-why-how-and-when" />
            <id>https://freek.dev/3166</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 PHP attributes, from metadata and Reflection to practical design patterns, Laravel examples, performance, testing, and the cases where explicit code is better.</p>


<a href='https://wendelladriel.com/blog/php-attributes-what-why-how-and-when'>Read more</a>]]>
            </summary>
                                    <updated>2026-07-21T14:43:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Supercharge your PHP apps with Go-powered PHP extensions]]></title>
            <link rel="alternate" href="https://freek.dev/3164-supercharge-your-php-apps-with-go-powered-php-extensions" />
            <id>https://freek.dev/3164</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A practical conference session on building PHP extensions with Go and FrankenPHP, then wiring them into Laravel and Symfony as native-feeling features. It uses an in-memory LRU cache as the example and offers a nice tour of framework flexibility and PHP internals.</p>


<a href='https://phpconference.nl/session/supercharge-your-php-apps-with-go-powered-php-extensions/'>Read more</a>]]>
            </summary>
                                    <updated>2026-07-17T14:30:31+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[New in PHP 8.6: Faster array_map with first-class callables]]></title>
            <link rel="alternate" href="https://freek.dev/3162-new-in-php-86-faster-array-map-with-first-class-callables" />
            <id>https://freek.dev/3162</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>In this video, Tideways shows how PHP 8.6 speeds up array_map when you use first-class callables.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Bbc0fXNXB3U" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
]]>
            </summary>
                                    <updated>2026-07-15T14:30:27+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[The Laravel Scheduler Under the Hood]]></title>
            <link rel="alternate" href="https://freek.dev/3161-the-laravel-scheduler-under-the-hood" />
            <id>https://freek.dev/3161</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 the Laravel Scheduler, from event registration and cron evaluation to mutexes, background processes, sub-minute tasks, and production-safe scheduling.</p>


<a href='https://wendelladriel.com/blog/the-laravel-scheduler-under-the-hood'>Read more</a>]]>
            </summary>
                                    <updated>2026-07-14T14:02:25+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Everything About the Context Facade]]></title>
            <link rel="alternate" href="https://freek.dev/3152-everything-about-the-context-facade" />
            <id>https://freek.dev/3152</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 Laravel Context, from request metadata and automatic log enrichment to hidden context, scoped values, queues, scheduled commands, and internals.</p>


<a href='https://wendelladriel.com/blog/everything-about-the-context-facade'>Read more</a>]]>
            </summary>
                                    <updated>2026-06-29T14:13:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Improving Laravel Architecture With Expressive]]></title>
            <link rel="alternate" href="https://freek.dev/3134-improving-laravel-architecture-with-expressive" />
            <id>https://freek.dev/3134</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Learn how Expressive can improve a Laravel application by keeping Eloquent as the database layer while moving business logic to fully typed objects.</p>


<a href='https://wendelladriel.com/blog/improving-laravel-architecture-with-expressive'>Read more</a>]]>
            </summary>
                                    <updated>2026-06-23T14:30:27+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Eloquent Query Classes Pattern]]></title>
            <link rel="alternate" href="https://freek.dev/3146-eloquent-query-classes-pattern" />
            <id>https://freek.dev/3146</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Learn how to use Eloquent Query Classes to organize important database logic in Laravel without adding a full repository layer.</p>


<a href='https://wendelladriel.com/blog/eloquent-query-classes-pattern'>Read more</a>]]>
            </summary>
                                    <updated>2026-06-22T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building RAG in Laravel: Four Ingestion Bugs That Silently Wreck Retrieval]]></title>
            <link rel="alternate" href="https://freek.dev/3144-building-rag-in-laravel-four-ingestion-bugs-that-silently-wreck-retrieval" />
            <id>https://freek.dev/3144</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Every Laravel RAG tutorial builds the same ingestion pipeline (chunk, embed, store) and stops the moment the agent answers on screen. None of them check whether retrieval is any good. But retrieval quality is decided at ingestion, before the model runs once, and four decisions there fail with no error, no exception, no failed test:</p>
<ul>
<li><strong>Chunking</strong> that severs the answer mid-sentence, so <code>answer@1</code> falls while <code>source hit@1</code> still looks healthy.</li>
<li>An <strong>HNSW index</strong> built with <code>vector_l2_ops</code> while you query with cosine <code>&lt;=&gt;</code>. Postgres silently ignores the index and scans every row. Laravel 13's native <code>whereVectorSimilarTo()</code> hardcodes <code>&lt;=&gt;</code>, so it's easier to hit than ever. Shown with <code>EXPLAIN</code>.</li>
<li>The <strong>embedding dimension</strong> baked into the <code>vector(1536)</code> column type, so &quot;shrink it to save storage&quot; is a migration plus a full re-embed that quietly drops retrieval to 47%.</li>
<li><strong>Ingesting and querying with different models</strong>, which turns every distance into noise.</li>
</ul>
<p>Each bug is real code from a working repo, proven against an eval suite. It's the prequel to my earlier &quot;Evaluating RAG in Laravel&quot; post: build it, prove it, tune it. Every example verified against <code>laravel/ai</code> v0.7.2 and pgvector, with the full repo to clone.</p>


<a href='https://mujahidabbas.dev/blog/building-rag-laravel-pgvector/'>Read more</a>]]>
            </summary>
                                    <updated>2026-06-19T14:32:25+02:00</updated>
        </entry>
    </feed>
