<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://www.yellowduck.be/pretty-atom-feed-v3.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <link href="https://www.yellowduck.be" rel="alternate"/>
  <link href="https://www.yellowduck.be/posts/feed" rel="self"/>
  <author>
    <name>Pieter Claerhout</name>
    <email>pieter@yellowduck.be</email>
  </author>
  <id>https://www.yellowduck.be/posts/feed</id>
  <title>🐥 YellowDuck.be</title>
  <updated>2026-06-18T17:00:00Z</updated>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/the-mysql-null-safe-equality-operator" rel="alternate"/>
    <content type="html">&lt;p&gt;If you&apos;ve worked with MySQL long enough, you&apos;ve probably been bitten by &lt;code&gt;NULL&lt;/code&gt; comparisons at least once. A query that &lt;em&gt;should&lt;/em&gt; return results returns nothing. A &lt;code&gt;WHERE&lt;/code&gt; clause that &lt;em&gt;should&lt;/em&gt; exclude a row doesn&apos;t. The culprit is almost always the three-valued logic of SQL — and the null-safe equality operator &lt;code&gt;&lt;=&gt;&lt;/code&gt; is one of the cleanest tools for dealing with it.&lt;/p&gt;
&lt;h1&gt;The problem with &lt;code&gt;=&lt;/code&gt; and &lt;code&gt;NULL&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;In SQL, &lt;code&gt;NULL&lt;/code&gt; represents the absence of a value — the unknown. Because of this, any comparison involving &lt;code&gt;NULL&lt;/code&gt; using the standard &lt;code&gt;=&lt;/code&gt; operator yields &lt;code&gt;NULL&lt;/code&gt; (not &lt;code&gt;TRUE&lt;/code&gt; or &lt;code&gt;FALSE&lt;/code&gt;), and &lt;code&gt;NULL&lt;/code&gt; is falsy in a &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;   -- NULL
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;      -- NULL
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;         -- 1 (TRUE)
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;deleted_at&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;  -- returns nothing
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The idiomatic fix is &lt;code&gt;IS NULL&lt;/code&gt; / &lt;code&gt;IS NOT NULL&lt;/code&gt;, but that only works for literal null checks. The moment you&apos;re comparing two columns — one or both of which might be &lt;code&gt;NULL&lt;/code&gt; — things get awkward fast.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;-- This silently drops rows where either column is NULL
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;orders&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;shipping_address&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;billing_address&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Enter &lt;code&gt;&lt;=&gt;&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;MySQL&apos;s null-safe equality operator &lt;code&gt;&lt;=&gt;&lt;/code&gt; behaves exactly like &lt;code&gt;=&lt;/code&gt;, except it treats &lt;code&gt;NULL&lt;/code&gt; as a comparable value. Two &lt;code&gt;NULL&lt;/code&gt;s are considered equal, and a &lt;code&gt;NULL&lt;/code&gt; compared to any non-null value is &lt;code&gt;FALSE&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;   -- 1 (TRUE)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;      -- 0 (FALSE)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;         -- 1 (TRUE)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;         -- 0 (FALSE)
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it safe to compare nullable columns directly:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;-- Correctly includes rows where both columns are NULL
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;orders&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;shipping_address&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;billing_address&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;A real-world example&lt;/h1&gt;
&lt;p&gt;Consider a polymorphic &lt;code&gt;subscriptions&lt;/code&gt; join table that maps users to subscribable entities (posts, documents, threads, etc.). The goal: return all subscribers &lt;em&gt;excluding&lt;/em&gt; the item&apos;s author, even when &lt;code&gt;author_id&lt;/code&gt; might be &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The naive approach breaks silently:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;WHERE subscriptions&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;user_id &lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;!=&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; posts&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;author_id
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;author_id&lt;/code&gt; is &lt;code&gt;NULL&lt;/code&gt;, this evaluates to &lt;code&gt;NULL&lt;/code&gt;, so the row is dropped — meaning a user who subscribes to a post with no author would incorrectly disappear from the result.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The null-safe fix:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;NOT &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;subscriptions&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;user_id &lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;author_id&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;posts&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;variable-member&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;subscriptions&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;item_id&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reads as: &lt;em&gt;&quot;keep this row unless the subscriber&apos;s user_id exactly matches the author_id, treating NULL as a concrete equal value.&quot;&lt;/em&gt; When &lt;code&gt;author_id&lt;/code&gt; is &lt;code&gt;NULL&lt;/code&gt; and &lt;code&gt;user_id&lt;/code&gt; is not, the &lt;code&gt;&lt;=&gt;&lt;/code&gt; returns &lt;code&gt;FALSE&lt;/code&gt;, so &lt;code&gt;NOT FALSE&lt;/code&gt; is &lt;code&gt;TRUE&lt;/code&gt; — the row is kept. Correct behaviour in all cases.&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;&lt;=&gt;&lt;/code&gt; vs. the alternatives&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Handles NULL?&lt;/th&gt;
&lt;th&gt;Readable?&lt;/th&gt;
&lt;th&gt;Standard SQL?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;col = val&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;col IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes (literal only)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COALESCE(col, &apos;&apos;) = COALESCE(val, &apos;&apos;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes (with sentinel)&lt;/td&gt;
&lt;td&gt;Passable&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(col = val OR (col IS NULL AND val IS NULL))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Verbose&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;col &lt;=&gt; val&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (MySQL/MariaDB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;COALESCE&lt;/code&gt; sentinel approach is fragile — you need to pick a value that can never appear in real data. The verbose &lt;code&gt;OR (IS NULL AND IS NULL)&lt;/code&gt; pattern works but is noisy. &lt;code&gt;&lt;=&gt;&lt;/code&gt; wins on brevity and correctness, at the cost of portability.&lt;/p&gt;
&lt;h1&gt;When to use it&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;&lt;=&gt;&lt;/code&gt; is a good fit when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Comparing two nullable columns&lt;/strong&gt; directly in a &lt;code&gt;WHERE&lt;/code&gt; or &lt;code&gt;JOIN&lt;/code&gt; condition.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Negating equality on nullable data&lt;/strong&gt; (&lt;code&gt;NOT (a &lt;=&gt; b)&lt;/code&gt; is cleaner than the alternative).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upsert / deduplication queries&lt;/strong&gt; where you need exact matching including null identity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generated columns or audit logic&lt;/strong&gt; where you want to detect whether a value actually changed, including transitions to/from &lt;code&gt;NULL&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Caveats&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;MySQL and MariaDB only.&lt;/strong&gt; The &lt;code&gt;&lt;=&gt;&lt;/code&gt; operator is not part of the SQL standard and is not available in PostgreSQL, SQLite, or SQL Server. If your codebase runs tests against SQLite (a common Laravel setup), any &lt;code&gt;&lt;=&gt;&lt;/code&gt; in a raw query will fail there.&lt;/p&gt;
&lt;p&gt;PostgreSQL&apos;s equivalent is &lt;code&gt;IS NOT DISTINCT FROM&lt;/code&gt; / &lt;code&gt;IS DISTINCT FROM&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-sql&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;-- PostgreSQL equivalent of &lt;=&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;col &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;DISTINCT&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; val
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;-- PostgreSQL equivalent of NOT (col &lt;=&gt; val)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;col &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;DISTINCT&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; val
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If cross-database portability matters, abstract the comparison behind a query scope or use the verbose but portable &lt;code&gt;OR (IS NULL AND IS NULL)&lt;/code&gt; form.&lt;/p&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;p&gt;The null-safe equality operator &lt;code&gt;&lt;=&gt;&lt;/code&gt; is one of those small MySQL features that, once you know it exists, saves you from a whole class of subtle bugs. It&apos;s most valuable when you need to compare nullable columns directly — particularly in negated conditions where the standard &lt;code&gt;!=&lt;/code&gt; would silently swallow &lt;code&gt;NULL&lt;/code&gt; rows. Just keep portability in mind: it&apos;s a MySQL/MariaDB extension, so make sure your test database matches your production database before reaching for it.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/mysql&quot;&gt;#mysql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sql&quot;&gt;#sql&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-18T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/the-mysql-null-safe-equality-operator</id>
    <title>🐥 The MySQL null-safe equality operator: &lt;=&gt;</title>
    <updated>2026-06-18T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/testing-that-laravel-events-fire-after-a-transaction-commits" rel="alternate"/>
    <content type="html">&lt;p&gt;A common source of bugs in Laravel applications is dispatching events &lt;em&gt;inside&lt;/em&gt; a database transaction. Listeners often kick off their own queries or even their own transactions — and if the outer transaction hasn&apos;t committed yet, you can end up with deadlocks, stale reads, or listeners that act on data that gets rolled back.&lt;/p&gt;
&lt;p&gt;The fix is straightforward: dispatch events &lt;em&gt;after&lt;/em&gt; the transaction commits. But how do you write a test that actually enforces this? Here&apos;s a reusable pattern.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;Consider an &lt;code&gt;OrderAction&lt;/code&gt; that saves an order inside a transaction and then fires an &lt;code&gt;OrderWasPlaced&lt;/code&gt; event:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;PlaceOrderAction&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;__invoke&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Cart&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Order&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;transaction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$order&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createMany&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;            
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;            &lt;span class=&quot;comment&quot;&gt;// ❌ Event fired inside the transaction — listeners run&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;            &lt;span class=&quot;comment&quot;&gt;//    while the order row is still locked.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;            &lt;span class=&quot;type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any listener that tries to read the same rows will block (or deadlock) because the transaction still holds row locks.&lt;/p&gt;
&lt;p&gt;The correct version moves the dispatch outside:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;PlaceOrderAction&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;__invoke&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Cart&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Order&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$order&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constant-builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;transaction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$order&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createMany&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// ✅ Transaction has committed — listeners can safely read/write.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;The test pattern&lt;/h1&gt;
&lt;p&gt;The key idea: register a real event listener &lt;em&gt;before&lt;/em&gt; the action runs, and capture &lt;code&gt;DB::transactionLevel()&lt;/code&gt; at the moment the event fires. If the event fires inside the transaction the level will be elevated; if it fires after the commit it will match the baseline.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Actions&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;PlaceOrderAction&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Events&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Illuminate&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Support&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Facades&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Bus&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Illuminate&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Support&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Facades&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Illuminate&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Support&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Facades&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;PHPUnit&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Framework&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Attributes&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Tests&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;PlaceOrderActionTest&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;TestCase&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;it_dispatches_order_was_placed_after_the_transaction_commits&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// Fake jobs/queues so side-effect listeners don&amp;#39;t cascade.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;type&quot;&gt;Bus&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;fake&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// Capture the DB nesting depth before the action runs.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// LazilyRefreshDatabase / DatabaseTransactions wraps every test&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// in its own transaction, so the baseline is typically 1, not 0.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$baselineLevel&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;transactionLevel&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$levelAtDispatch&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constant-builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// Register a real listener — do NOT call Event::fake(), otherwise&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// the dispatcher is replaced with a mock and no listeners run.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;        &lt;span class=&quot;type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$levelAtDispatch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$levelAtDispatch&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;transactionLevel&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$cart&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Cart&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;withLines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;PlaceOrderAction&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;34&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;35&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;36&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$baselineLevel&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;37&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$levelAtDispatch&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;38&quot;&gt;            &lt;span class=&quot;string&quot;&gt;&amp;#39;OrderWasPlaced must be dispatched after the transaction commits, not inside it.&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;39&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;40&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;41&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why compare against &lt;code&gt;$baselineLevel&lt;/code&gt; instead of &lt;code&gt;0&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;Most Laravel test suites use &lt;code&gt;LazilyRefreshDatabase&lt;/code&gt; or &lt;code&gt;DatabaseTransactions&lt;/code&gt;, which wrap every test in an outer transaction for easy rollback. That means &lt;code&gt;DB::transactionLevel()&lt;/code&gt; starts at &lt;code&gt;1&lt;/code&gt; when the test body begins — not &lt;code&gt;0&lt;/code&gt;. Comparing against the snapshot taken &lt;em&gt;before&lt;/em&gt; the action runs is always correct, regardless of your test database strategy.&lt;/p&gt;
&lt;h1&gt;Handling listener cascades&lt;/h1&gt;
&lt;p&gt;Sometimes the event you want to observe triggers further actions that write to the database, causing failures when those writes reference data that doesn&apos;t exist in the current test context. Two strategies:&lt;/p&gt;
&lt;h2&gt;1. Fake only the cascading action&lt;/h2&gt;
&lt;p&gt;If a listener dispatches a secondary action (e.g. &lt;code&gt;StartFulfillmentAction&lt;/code&gt;), fake just that class so the chain stops there while leaving the event dispatcher intact:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;type&quot;&gt;Action&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;fake&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;StartFulfillmentAction&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The event still fires and your transaction-level listener still runs.&lt;/p&gt;
&lt;h2&gt;2. Fake only the jobs&lt;/h2&gt;
&lt;p&gt;If listeners queue jobs (Horizon, etc.), &lt;code&gt;Bus::fake()&lt;/code&gt; is usually enough to prevent the cascade without touching the event system at all.&lt;/p&gt;
&lt;h1&gt;Checking multiple events&lt;/h1&gt;
&lt;p&gt;To assert that &lt;em&gt;all&lt;/em&gt; the events fired by an action respect the post-commit invariant, collect them all in a single map:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$levels&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;InventoryReserved&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;InvoiceQueued&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;transactionLevel&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;PlaceOrderAction&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;array_keys&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$baselineLevel&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; must be dispatched outside the transaction.&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Also assert every expected event actually fired.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertSame&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;InventoryReserved&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;InvoiceQueued&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;array_keys&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Quick reference&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single event&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Event::listen()&lt;/code&gt; + &lt;code&gt;DB::transactionLevel()&lt;/code&gt; snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple events&lt;/td&gt;
&lt;td&gt;Loop over event classes, collect levels into a map&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Listener cascade breaks the test&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Action::fake(CascadingAction::class)&lt;/code&gt; or &lt;code&gt;Bus::fake()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Using &lt;code&gt;Event::fake()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ Replaces the dispatcher — listeners never run, pattern breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction depth varies by test setup&lt;/td&gt;
&lt;td&gt;Always snapshot &lt;code&gt;$baselineLevel&lt;/code&gt; before the action, never hardcode &lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The pattern is lightweight — no mocking frameworks, no custom test doubles, just a listener closure and a single assertion. Once you&apos;ve added it to one action test, it&apos;s easy to copy across the codebase anywhere you need to enforce the same invariant.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/database&quot;&gt;#database&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/testing&quot;&gt;#testing&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-16T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/testing-that-laravel-events-fire-after-a-transaction-commits</id>
    <title>🐥 Testing that Laravel events fire after a transaction commits</title>
    <updated>2026-06-16T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/speeding-up-s3-uploads-in-github-actions-with-bash-parallelism" rel="alternate"/>
    <content type="html">&lt;p&gt;When you&apos;re uploading multiple directories to S3 (or an S3-compatible CDN like DigitalOcean Spaces) in a CI pipeline, the naive approach runs each upload sequentially. If you have three directories and each takes 30 seconds, you&apos;re waiting 90 seconds. They&apos;re completely independent — there&apos;s no reason not to run them at the same time.&lt;/p&gt;
&lt;p&gt;Here&apos;s how to parallelize them with nothing but bash.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;A typical multi-directory upload step looks like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;Upload to CDN&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;public/build&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--acl-public&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;public/img&lt;/span&gt;   &lt;span class=&quot;variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--acl-public&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;public/js&lt;/span&gt;    &lt;span class=&quot;variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--acl-public&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each &lt;code&gt;s3cmd put&lt;/code&gt; blocks until it&apos;s done before the next one starts. Wall-clock time = sum of all three.&lt;/p&gt;
&lt;h1&gt;The fix&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-yaml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;Upload to CDN&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;public/build&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--acl-public&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;public/img&lt;/span&gt;   &lt;span class=&quot;variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--acl-public&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;public/js&lt;/span&gt;    &lt;span class=&quot;variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--acl-public&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;character-special&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;wait&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;done&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wall-clock time = duration of the slowest upload.&lt;/p&gt;
&lt;h1&gt;How it works&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp; pids+=($!)&lt;/code&gt;&lt;/strong&gt; — The &lt;code&gt;&amp;&lt;/code&gt; runs the command in the background. &lt;code&gt;$!&lt;/code&gt; is bash&apos;s special variable for the PID of the last backgrounded process, and we immediately append it to the &lt;code&gt;pids&lt;/code&gt; array before starting the next job.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;for pid in &quot;$&amp;lbrace;pids[@]&amp;rbrace;&quot;; do wait &quot;$pid&quot; || exit 1; done&lt;/code&gt;&lt;/strong&gt; — We wait for each background job by PID and fail the step immediately if any one of them exits with a non-zero code. This is important: a plain &lt;code&gt;wait&lt;/code&gt; without arguments returns the exit code of the &lt;em&gt;last&lt;/em&gt; process it waited for, which means a failure in the first or second upload could go undetected.&lt;/p&gt;
&lt;h1&gt;Why not just &lt;code&gt;wait&lt;/code&gt;?&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# Dangerous — only checks the exit code of the last job&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;public/build&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;public/img&lt;/span&gt;   &lt;span class=&quot;variable-parameter&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;public/js&lt;/span&gt;    &lt;span class=&quot;variable-parameter&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;wait&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;public/build&lt;/code&gt; fails but &lt;code&gt;public/js&lt;/code&gt; succeeds, this exits 0 and your CI run goes green with a broken CDN.&lt;/p&gt;
&lt;p&gt;Waiting by PID and checking each one individually gives you the same safety guarantee as running sequentially, at the speed of the fastest possible parallel execution.&lt;/p&gt;
&lt;h1&gt;The general pattern&lt;/h1&gt;
&lt;p&gt;This technique works for any set of independent shell commands you want to parallelize:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;some-command&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;arg1&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;some-command&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;arg2&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;some-command&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;arg3&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;character-special&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;wait&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;done&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No extra tooling, no GNU Parallel, no xargs — just bash.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/github&quot;&gt;#github&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-14T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/speeding-up-s3-uploads-in-github-actions-with-bash-parallelism</id>
    <title>🐥 Speeding up S3 uploads in GitHub Actions with Bash parallelism</title>
    <updated>2026-06-14T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/debugging-phpunit-notices-in-laravel-parallel-tests" rel="alternate"/>
    <content type="html">&lt;p&gt;Running Laravel&apos;s test suite in parallel speeds things up considerably, but it also makes it easy to miss PHPUnit notices. The parallel worker output gets interleaved and buffered, and notices about deprecated API usage or risky tests tend to scroll past unnoticed — or disappear entirely. This post shows the command I use to surface them reliably.&lt;/p&gt;
&lt;h1&gt;The command&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;LARAVEL_PARALLEL_TESTING&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;vendor/brianium/paratest/bin/paratest&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;--colors=always&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;--configuration=/path/to/phpunit.xml&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;--runner=\\Illuminate\\Testing\\ParallelRunner&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;--display-phpunit-notices&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;--fail-on-all-issues&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;tests/Unit&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What each part does&lt;/h1&gt;
&lt;h2&gt;Environment variables&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;LARAVEL_PARALLEL_TESTING=1&lt;/code&gt; activates Laravel&apos;s parallel testing support. It causes the framework to spin up separate database connections per worker (suffixed &lt;code&gt;_1&lt;/code&gt;, &lt;code&gt;_2&lt;/code&gt;, etc.) and seed each one independently.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES=1&lt;/code&gt; forces those databases to be dropped and recreated from scratch on every run. Without it, a previous run&apos;s leftover state can cause tests to pass or fail for the wrong reasons — particularly relevant when you change a migration between runs.&lt;/p&gt;
&lt;h2&gt;Invoking paratest directly&lt;/h2&gt;
&lt;p&gt;Laravel&apos;s &lt;code&gt;php artisan test --parallel&lt;/code&gt; is a thin wrapper around &lt;code&gt;brianium/paratest&lt;/code&gt;. Calling the binary directly gives you access to flags that the Artisan wrapper doesn&apos;t expose, particularly the notice-related ones below.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--runner=\\Illuminate\\Testing\\ParallelRunner&lt;/code&gt; tells paratest to use Laravel&apos;s own runner class, which handles the database token injection and other framework-specific setup that the default runner skips.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--configuration&lt;/code&gt; takes an absolute path to your &lt;code&gt;phpunit.xml&lt;/code&gt;. When you invoke paratest from outside the project root — for example, from a CI script or a Makefile — relative paths silently resolve to the wrong location. Using an absolute path avoids that class of silent misconfiguration.&lt;/p&gt;
&lt;h2&gt;Surfacing notices&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--display-phpunit-notices&lt;/code&gt; is the key flag. PHPUnit emits notices for things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;calls to deprecated assertion methods (&lt;code&gt;assertContains&lt;/code&gt; on a string instead of &lt;code&gt;assertStringContainsString&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;tests marked &lt;code&gt;@covers&lt;/code&gt; that cover no code&lt;/li&gt;
&lt;li&gt;tests with no assertions when &lt;code&gt;beStrictAboutTestsThatDoNotTestAnything&lt;/code&gt; is enabled&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a parallel run these notices are buffered per worker and often never reach the terminal. This flag ensures they are printed to output regardless.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--fail-on-all-issues&lt;/code&gt; treats any notice, warning, or deprecation as a test suite failure. This is what makes the command useful for CI: the exit code becomes non-zero the moment any worker emits a notice, so the pipeline fails and forces you to deal with it rather than letting it accumulate.&lt;/p&gt;
&lt;h2&gt;Scoping to &lt;code&gt;tests/Unit&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Passing &lt;code&gt;tests/Unit&lt;/code&gt; as the path restricts the run to unit tests, which tends to surface notices faster than a full suite run because unit tests don&apos;t need a running server or real database queries. Once you&apos;ve cleared the unit test notices, you can repeat with &lt;code&gt;tests/Feature&lt;/code&gt; or omit the path entirely.&lt;/p&gt;
&lt;h1&gt;Reading the output&lt;/h1&gt;
&lt;p&gt;When a notice fires, paratest prints it alongside the failing worker output. The format looks like:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;NOTICE&lt;/span&gt;  &lt;span class=&quot;variable-parameter&quot;&gt;tests/Unit/Services/OrderServiceTest.php:42&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;Method&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Illuminate\Testing\Assert::assertContains&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;deprecated.&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Use&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;assertStringContainsString&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;instead.&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file path and line number point directly to the test method that triggered it. If you see the same notice repeating across many tests, the issue is usually in a shared base class or a trait — check the stack trace for the actual call site rather than fixing every test individually.&lt;/p&gt;
&lt;h1&gt;Making it a habit&lt;/h1&gt;
&lt;p&gt;The goal is to run with &lt;code&gt;--fail-on-all-issues&lt;/code&gt; in CI from the start, before notices accumulate. If you&apos;re adding this to an existing codebase, it&apos;s usually easier to tackle notices test-file by test-file: run against a single file first, fix what you find, then broaden the path incrementally.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# Start with one file&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;constant&quot;&gt;LARAVEL_PARALLEL_TESTING&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;vendor/brianium/paratest/bin/paratest&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;--runner=\\Illuminate\\Testing\\ParallelRunner&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;--display-phpunit-notices&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;--fail-on-all-issues&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;tests/Unit/Services/OrderServiceTest.php&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the file is clean, commit and move on to the next. The &lt;code&gt;--fail-on-all-issues&lt;/code&gt; flag acts as a ratchet: once a file is notice-free, CI will catch any regression immediately.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/testing&quot;&gt;#testing&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-12T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/debugging-phpunit-notices-in-laravel-parallel-tests</id>
    <title>🐥 Debugging PHPUnit notices in Laravel parallel tests</title>
    <updated>2026-06-12T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/from-n-1-to-1-extracting-paged-pdf-text-with-a-single-pdftotext-call" rel="alternate"/>
    <content type="html">&lt;p&gt;When building full-text search for uploaded documents, we needed to extract text page-by-page from PDFs so we could index each page as a separate chunk. The naive approach worked but was painfully slow. Here&apos;s how a single Unix insight cut it down to one process spawn.&lt;/p&gt;
&lt;h1&gt;The problem: N+1 process spawns&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;pdftotext&lt;/code&gt; is the standard Unix utility for extracting text from PDFs. It supports a &lt;code&gt;-f&lt;/code&gt; (first page) and &lt;code&gt;-l&lt;/code&gt; (last page) flag, so extracting a single page looks like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;pdftotext&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;document.pdf&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The natural implementation for paged extraction is to call this in a loop:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;getPagedTextFromFile&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Collection&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$pagedText&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$pageCount&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;pdfInfoService&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getPageCountFromFile&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$pageCount&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getRawTextFromFile&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is clean and obvious. It is also, for any document with real content, expensive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 &lt;code&gt;pdfinfo&lt;/code&gt; call to get the page count&lt;/li&gt;
&lt;li&gt;N &lt;code&gt;pdftotext&lt;/code&gt; calls, one per page&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each call is a separate process spawn: the OS forks, loads the binary, opens the PDF, seeks to the requested page, extracts text, and exits. For a 50-page contract, that&apos;s 51 process spawns. On a server handling concurrent uploads, those 51 spawns happen in sequence, blocking the queue worker the entire time.&lt;/p&gt;
&lt;h1&gt;The insight: pdftotext already separates pages&lt;/h1&gt;
&lt;p&gt;Run &lt;code&gt;pdftotext&lt;/code&gt; without page flags and pipe the output to a hex viewer:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;pdftotext&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;document.pdf&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-P&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;\f&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&apos;ll see form feed characters (&lt;code&gt;\f&lt;/code&gt;, &lt;code&gt;\x0C&lt;/code&gt;, ASCII 12) separating each page. This is standard — &lt;code&gt;pdftotext&lt;/code&gt; has always done this. It&apos;s even documented in the man page, buried under the output format description.&lt;/p&gt;
&lt;p&gt;That means the full multi-page text is already structured. We don&apos;t need N calls. We need one call and a string split.&lt;/p&gt;
&lt;h1&gt;The solution&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;getPagedTextFromFile&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Collection&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$pagedText&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;keyword-exception&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;mimeTypeForPath&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;application/pdf&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;            &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;runExternalProcess&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;pdftools.pdftotext&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;            &lt;span class=&quot;number&quot;&gt;180&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getStdErr&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;            &lt;span class=&quot;keyword-exception&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Exception&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getStdErr&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;// pdftotext separates pages with \f; rtrim strips any optional trailing \f&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$pages&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;explode&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\f&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;rtrim&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getStdOut&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\f&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;        &lt;span class=&quot;keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$pages&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$pageText&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$pageText&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot; &lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\t&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\r&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\x0B&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;            &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;                &lt;span class=&quot;variable&quot;&gt;$lines&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;splitInLines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$pageText&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;                &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$lines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;preg_match&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-builtin&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;DOCUSIGN_HEADER_PATTERN&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$lines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;                    &lt;span class=&quot;function-call&quot;&gt;array_shift&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$lines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;                    &lt;span class=&quot;variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;implode&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$lines&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot; &lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\t&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\r&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;string-escape&quot;&gt;\x0B&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;                &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;            &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;34&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;35&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span class=&quot;keyword-conditional-ternary&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;keyword-conditional-ternary&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;constant-builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;36&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;37&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;keyword-exception&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;38&quot;&gt;        &lt;span class=&quot;type&quot;&gt;Log&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;pdftotext | &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getMessage&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;39&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;40&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;41&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;42&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; 51 process spawns for a 50-page PDF.&lt;br /&gt;
&lt;strong&gt;After:&lt;/strong&gt; 1 process spawn, regardless of page count.&lt;/p&gt;
&lt;p&gt;For a 100-page document the old approach invoked &lt;code&gt;pdftotext&lt;/code&gt; 100 times, each time reloading and re-parsing the entire PDF file to seek to one page. The new approach loads it once and returns everything.&lt;/p&gt;
&lt;h1&gt;Edge cases worth knowing&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Trailing form feed.&lt;/strong&gt; Some versions of &lt;code&gt;pdftotext&lt;/code&gt; append a &lt;code&gt;\f&lt;/code&gt; after the last page. Splitting &lt;code&gt;&quot;page1\fpage2\f&quot;&lt;/code&gt; naively gives &lt;code&gt;[&quot;page1&quot;, &quot;page2&quot;, &quot;&quot;]&lt;/code&gt; — an extra empty element. The &lt;code&gt;rtrim($output, &quot;\f&quot;)&lt;/code&gt; before splitting removes it cleanly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blank pages.&lt;/strong&gt; A blank page produces an empty string after trimming. Storing &lt;code&gt;null&lt;/code&gt; for it preserves correct page numbering for subsequent pages (page 5 stays page 5, even if pages 3 and 4 are blank). The downstream indexing code skips nulls, so blank pages don&apos;t pollute the search index.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Page-level headers.&lt;/strong&gt; We strip DocuSign envelope headers (&lt;code&gt;DocuSign Envelope ID: XXXXXXXX-...&lt;/code&gt;) from the beginning of any page that has one. Since the single-call output is split by page before this check, the per-page stripping logic is identical to the per-call approach — just applied after the split instead of inside each &lt;code&gt;getRawTextFromFile&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Non-PDF files.&lt;/strong&gt; The MIME type check at the top of the method returns early for anything that isn&apos;t &lt;code&gt;application/pdf&lt;/code&gt;. This replaces the earlier dependency on &lt;code&gt;pdfinfo&lt;/code&gt; to get the page count — for non-PDFs that check would have returned 0 and short-circuited the loop, but the MIME check is simpler and removes the &lt;code&gt;pdfinfo&lt;/code&gt; dependency from this path entirely.&lt;/p&gt;
&lt;h1&gt;The broader pattern&lt;/h1&gt;
&lt;p&gt;This is an instance of a general optimisation: if a tool is designed to process a whole file, don&apos;t call it once per chunk. The tool already knows how to walk the file efficiently; let it.&lt;/p&gt;
&lt;p&gt;The same principle applies to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ffprobe&lt;/code&gt; for video metadata — one call for all streams, not one call per stream&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exiftool&lt;/code&gt; — batch mode processes a directory in one pass rather than per-file invocations&lt;/li&gt;
&lt;li&gt;Database queries — &lt;code&gt;SELECT&lt;/code&gt; with &lt;code&gt;IN (...)&lt;/code&gt; instead of N individual selects&lt;/li&gt;
&lt;li&gt;Meilisearch document uploads — one &lt;code&gt;addDocuments&lt;/code&gt; call with a batch, not one call per document&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In each case the per-item call pattern feels natural and is easy to reason about. But the overhead of repeatedly invoking a tool that was designed for whole-file processing accumulates fast once documents are large or queues are busy.&lt;/p&gt;
&lt;p&gt;The fix, when it exists, is usually as simple as this one: read the man page, find the output format, split a string.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/pdf&quot;&gt;#pdf&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-10T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/from-n-1-to-1-extracting-paged-pdf-text-with-a-single-pdftotext-call</id>
    <title>🐥 From N+1 to 1: Extracting paged PDF text with a single pdftotext call</title>
    <updated>2026-06-10T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/the-silent-saboteur-how-quiet-can-break-your-laravel-command-sequences" rel="alternate"/>
    <content type="html">&lt;p&gt;When you chain multiple Artisan commands together in Laravel, there&apos;s a subtle trap waiting for you — one that&apos;s easy to miss because the commands still &lt;em&gt;run&lt;/em&gt;, they just stop &lt;em&gt;talking&lt;/em&gt;.&lt;/p&gt;
&lt;h1&gt;The setup&lt;/h1&gt;
&lt;p&gt;A common pattern in Laravel applications is a &quot;meta-command&quot; that runs a sequence of other commands in order. Think of a nightly job runner:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;int&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$commandsToRun&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;type&quot;&gt;SomeCommand&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;        &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;type&quot;&gt;AnotherCommand&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;     &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;type&quot;&gt;DatabasePruneCommand&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;--quiet&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;type&quot;&gt;MoreCommands&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;       &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;type&quot;&gt;FinalCommand&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;       &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$commandsToRun&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$commandToRun&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$parameters&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$commandToRun&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;type&quot;&gt;Artisan&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$commandToRun&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$parameters&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You pass &lt;code&gt;$this-&gt;output&lt;/code&gt; into each &lt;code&gt;Artisan::call&lt;/code&gt; so the sub-command&apos;s output flows through to the console. Clean and straightforward.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;One of your commands — say, a prune command — is noisy by default. You don&apos;t want its output cluttering the logs, so you pass &lt;code&gt;&apos;--quiet&apos; =&gt; true&lt;/code&gt;. The title still appears, the command runs, everything looks fine.&lt;/p&gt;
&lt;p&gt;Then you notice something odd: the commands &lt;em&gt;after&lt;/em&gt; that quiet one stop printing their titles. The log files confirm they&apos;re running, but the console goes dark after that one &lt;code&gt;--quiet&lt;/code&gt; call.&lt;/p&gt;
&lt;h1&gt;Why it happens&lt;/h1&gt;
&lt;p&gt;When Laravel processes the &lt;code&gt;--quiet&lt;/code&gt; flag on a command, it calls:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$output&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;setVerbosity&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;OutputInterface&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;VERBOSITY_QUIET&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key word is &lt;strong&gt;on the output object you passed in&lt;/strong&gt;. Because you shared &lt;code&gt;$this-&gt;output&lt;/code&gt; across all &lt;code&gt;Artisan::call&lt;/code&gt; invocations, that single &lt;code&gt;setVerbosity&lt;/code&gt; call mutates the object in place. Every subsequent command — and every &lt;code&gt;title()&lt;/code&gt;, &lt;code&gt;info()&lt;/code&gt;, or &lt;code&gt;line()&lt;/code&gt; call — now runs against a quietly-configured output. They&apos;re suppressed silently, with no error.&lt;/p&gt;
&lt;p&gt;The commands still execute; they just can&apos;t speak.&lt;/p&gt;
&lt;h1&gt;The fix&lt;/h1&gt;
&lt;p&gt;Capture the verbosity before each call and restore it immediately after:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$commandsToRun&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$commandToRun&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$parameters&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$commandToRun&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$verbosity&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getVerbosity&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;type&quot;&gt;Artisan&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$commandToRun&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$parameters&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;setVerbosity&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$verbosity&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two lines. The sub-command can do whatever it likes to the output during its run; your sequence always gets a clean slate for the next iteration.&lt;/p&gt;
&lt;h1&gt;The lesson&lt;/h1&gt;
&lt;p&gt;Shared mutable objects are the classic source of action-at-a-distance bugs. The output object here is a perfect example: you pass it in expecting read-like behaviour (writing to the terminal), but the callee has full write access to its configuration too.&lt;/p&gt;
&lt;p&gt;A few takeaways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Passing an object &quot;for output&quot; also grants mutation rights.&lt;/strong&gt; Laravel&apos;s &lt;code&gt;OutputStyle&lt;/code&gt; is stateful — verbosity, decorations, and more can be changed by anyone holding a reference.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commands that still run aren&apos;t necessarily commands that are working correctly.&lt;/strong&gt; Silent output suppression looks identical to &quot;nothing went wrong&quot; if you&apos;re only checking exit codes or log files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test the observable side effects, not just execution.&lt;/strong&gt; A test that asserts &lt;code&gt;title()&lt;/code&gt; is called N times would have caught this immediately; a test that only checks &lt;code&gt;Artisan::call&lt;/code&gt; was invoked N times would not.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Bonus: write the test first&lt;/h1&gt;
&lt;p&gt;If you&apos;d written this test before the bug appeared, you&apos;d have been protected from day one:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;it_restores_verbosity_so_quiet_commands_do_not_suppress_subsequent_output&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// Simulate a sub-command that sets quiet mode&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;type&quot;&gt;Artisan&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;shouldReceive&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;call&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;twice&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;andReturnUsing&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$output&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;            &lt;span class=&quot;variable&quot;&gt;$output&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;setVerbosity&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;OutputInterface&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;VERBOSITY_QUIET&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;            &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;// Both titles must still appear&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$output&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;expects&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;exactly&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;title&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;subject&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;runSequenceOfCommands&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$command&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;first:command&amp;#39;&lt;/span&gt;  &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;second:command&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shared mutable state is everywhere in framework code. A little defensive save-and-restore goes a long way.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-08T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/the-silent-saboteur-how-quiet-can-break-your-laravel-command-sequences</id>
    <title>🐥 The silent saboteur: how --quiet can break your Laravel command sequences</title>
    <updated>2026-06-08T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/how-to-check-how-much-memory-a-systemd-unit-is-actually-using" rel="alternate"/>
    <content type="html">&lt;p&gt;When investigating memory usage on a Linux server, it&apos;s common to look at processes using tools such as &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;htop&lt;/code&gt;, or &lt;code&gt;ps&lt;/code&gt;. However, when applications are managed by systemd, it is often more useful to inspect memory usage at the service level rather than at the individual process level.&lt;/p&gt;
&lt;p&gt;This is especially important for services that spawn multiple worker processes.&lt;/p&gt;
&lt;h1&gt;Using systemctl&lt;/h1&gt;
&lt;p&gt;The easiest way to see the memory usage of a systemd unit is:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;Memory: 423.1M
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;CPU: 12min 34.567s
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This value represents the total memory usage of the service&apos;s cgroup, including all child processes managed by the unit.&lt;/p&gt;
&lt;h1&gt;Using systemctl show&lt;/h1&gt;
&lt;p&gt;For scripting and automation, use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--property=MemoryCurrent&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;MemoryCurrent=443662336
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The value is returned in bytes.&lt;/p&gt;
&lt;p&gt;To convert it to megabytes:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--property=MemoryCurrent&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--value&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;&amp;lbrace; printf &quot;%.2f MB\n&quot;, $1 / 1024 / 1024 &amp;rbrace;&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Inspecting the cgroup directly&lt;/h1&gt;
&lt;p&gt;Systemd tracks resource usage through Linux cgroups.&lt;/p&gt;
&lt;p&gt;You can inspect the memory consumption directly:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/sys/fs/cgroup/system.slice/my-service.service/memory.current&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or locate the cgroup first:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--property=ControlGroup&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;ControlGroup=/system.slice/my-service.service
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then inspect the corresponding cgroup files.&lt;/p&gt;
&lt;h1&gt;Finding the processes behind a service&lt;/h1&gt;
&lt;p&gt;To see which processes belong to a unit:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--property=MainPID&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then inspect individual processes:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;ps&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;pid,rss,vsz,cmd&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;string-special-path&quot;&gt;PID&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep in mind that summing RSS values from multiple processes can overestimate actual memory usage because shared memory pages may be counted multiple times.&lt;/p&gt;
&lt;h1&gt;Monitoring memory usage over time&lt;/h1&gt;
&lt;p&gt;For live monitoring:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;watch&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;systemctl show my-service.service --property=MemoryCurrent --value&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemd-cgtop&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tool displays CPU, memory, and I/O usage per cgroup and is often the most convenient way to identify memory-hungry services on a system.&lt;/p&gt;
&lt;h1&gt;Configuring memory limits&lt;/h1&gt;
&lt;p&gt;Systemd can also enforce memory limits.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-ini&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;markup-heading&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;property&quot;&gt;MemoryMax&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;1G&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After updating the unit file:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;daemon-reload&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;restart&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To verify the configured limit:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--property=MemoryMax&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;For services managed by systemd, &lt;code&gt;MemoryCurrent&lt;/code&gt; is usually the most accurate representation of how much memory the service is actually consuming. Unlike process-level tools, it accounts for the entire cgroup, making it ideal for monitoring applications that use worker pools, background jobs, or multiple child processes.&lt;/p&gt;
&lt;p&gt;Useful commands to remember:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--property=MemoryCurrent&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;systemd-cgtop&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These provide a much clearer picture of service-level memory usage than inspecting individual processes alone.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sysadmin&quot;&gt;#sysadmin&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-06T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/how-to-check-how-much-memory-a-systemd-unit-is-actually-using</id>
    <title>🐥 How to check how much memory a systemd unit is actually using</title>
    <updated>2026-06-06T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/fixing-phpunit-13-with-without-expects-deprecations-in-laravel-tests" rel="alternate"/>
    <content type="html">&lt;p&gt;After upgrading to PHPUnit 13, you may run into this deprecation warning in Laravel test suites:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;Using with*() without expects() is deprecated and will no longer be possible in PHPUnit 14.
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first sight, the code often already looks correct:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$mock&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;expects&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So what is actually causing the warning?&lt;/p&gt;
&lt;h1&gt;The real issue&lt;/h1&gt;
&lt;p&gt;In many Laravel applications, the problem comes from using Laravel’s &lt;code&gt;createPartialMock()&lt;/code&gt; helper together with newer PHPUnit versions.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createPartialMock&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this worked fine in older PHPUnit versions, PHPUnit 13 tightened internal mock expectation handling and now emits deprecation warnings in some cases when using Laravel’s wrapper helpers.&lt;/p&gt;
&lt;h1&gt;The fix&lt;/h1&gt;
&lt;p&gt;Instead of &lt;code&gt;createPartialMock()&lt;/code&gt;, use PHPUnit’s native mock builder API.&lt;/p&gt;
&lt;p&gt;Replace this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createPartialMock&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getMockBuilder&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;onlyMethods&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getMock&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And for multiple mocked methods:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getMockBuilder&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;onlyMethods&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;confirm&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getMock&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why this works&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;getMockBuilder()&lt;/code&gt; uses PHPUnit’s modern native mock API directly, avoiding the legacy compatibility layer behind Laravel’s partial mock helpers.&lt;/p&gt;
&lt;p&gt;This makes your tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;compatible with PHPUnit 13&lt;/li&gt;
&lt;li&gt;future-proof for PHPUnit 14&lt;/li&gt;
&lt;li&gt;less dependent on framework-specific mock wrappers&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Final result&lt;/h1&gt;
&lt;p&gt;Your expectations can stay exactly the same:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;$class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;expects&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;isInstanceOf&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;comment&quot;&gt;// expected commands&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But by switching to &lt;code&gt;getMockBuilder()&lt;/code&gt;, the deprecation warning disappears cleanly.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/testing&quot;&gt;#testing&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-04T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/fixing-phpunit-13-with-without-expects-deprecations-in-laravel-tests</id>
    <title>🐥 Fixing PHPUnit 13 with*() without expects() deprecations in Laravel tests</title>
    <updated>2026-06-04T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/why-crypto-getrandomvalues-matters-in-javascript" rel="alternate"/>
    <content type="html">&lt;p&gt;Generating random values sounds simple, until you need randomness that is actually secure.&lt;/p&gt;
&lt;p&gt;A lot of JavaScript developers reach for &lt;code&gt;Math.random()&lt;/code&gt; out of habit. While that works fine for visual effects, games, or non-critical IDs, it should never be used for anything security-sensitive.&lt;/p&gt;
&lt;p&gt;That’s where the Web Crypto API comes in.&lt;/p&gt;
&lt;h1&gt;The problem with &lt;code&gt;Math.random()&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Math.random()&lt;/code&gt; is not cryptographically secure.&lt;/p&gt;
&lt;p&gt;Its output is deterministic and predictable enough that an attacker may be able to reproduce or guess generated values under certain conditions.&lt;/p&gt;
&lt;p&gt;That makes it unsuitable for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Session tokens&lt;/li&gt;
&lt;li&gt;Password reset links&lt;/li&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;CSRF tokens&lt;/li&gt;
&lt;li&gt;Encryption keys&lt;/li&gt;
&lt;li&gt;Secure identifiers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;36&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This may look random, but it is not secure.&lt;/p&gt;
&lt;h1&gt;Using &lt;code&gt;crypto.getRandomValues()&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Modern browsers provide a secure random number generator through the Web Crypto API.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This fills the typed array with cryptographically secure random bytes provided by the operating system.&lt;/p&gt;
&lt;h1&gt;Generating a secure random token&lt;/h1&gt;
&lt;p&gt;A common use case is generating secure tokens or identifiers.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;byte&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;padStart&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;0&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;4f8b7d3a1f9e0c8d7a2b6c5d4e3f1a9c
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is significantly safer than using &lt;code&gt;Math.random()&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Typed arrays are required&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;crypto.getRandomValues()&lt;/code&gt; only works with integer-based typed arrays.&lt;/p&gt;
&lt;p&gt;Supported examples include:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint16Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Int32Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will fail:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because regular JavaScript arrays are not supported.&lt;/p&gt;
&lt;h1&gt;Browser and runtime support&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;crypto.getRandomValues()&lt;/code&gt; is widely supported in modern browsers.&lt;/p&gt;
&lt;p&gt;It is also available in runtimes like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Deno&lt;/li&gt;
&lt;li&gt;Bun&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example in Node.js:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In older Node.js versions, developers typically used:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;crypto&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;randomBytes&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;UUID generation&lt;/h1&gt;
&lt;p&gt;If your goal is generating UUIDs, modern runtimes also support:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;550e8400-e29b-41d4-a716-446655440000
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Internally, this also uses cryptographically secure randomness.&lt;/p&gt;
&lt;h1&gt;Things to remember&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;Math.random()&lt;/code&gt; for non-security-related randomness only&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;crypto.getRandomValues()&lt;/code&gt; for anything security-sensitive&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;crypto.randomUUID()&lt;/code&gt; when generating UUIDs&lt;/li&gt;
&lt;li&gt;Always generate randomness using the operating system’s secure RNG&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For modern JavaScript applications, &lt;code&gt;crypto.getRandomValues()&lt;/code&gt; should be the default choice whenever security matters.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/javascript&quot;&gt;#javascript&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/auth&quot;&gt;#auth&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-06-02T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/why-crypto-getrandomvalues-matters-in-javascript</id>
    <title>🐥 Why crypto.getRandomValues() matters in JavaScript</title>
    <updated>2026-06-02T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/how-to-find-all-jira-issues-ever-assigned-to-someone-even-historical-ones" rel="alternate"/>
    <content type="html">&lt;p&gt;If you&apos;ve ever tried to pull up all the tickets that &lt;strong&gt;Sarah Mitchell&lt;/strong&gt; worked on last year, you&apos;ve probably started with the obvious query:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assignee = &quot;sarah.mitchell@example.com&quot;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then realised it only shows her &lt;em&gt;current&lt;/em&gt; tickets — not the ones she handed off to &lt;strong&gt;Tom Bergkamp&lt;/strong&gt; or closed six months ago.&lt;/p&gt;
&lt;p&gt;Here&apos;s the fix.&lt;/p&gt;
&lt;h1&gt;The &lt;code&gt;was&lt;/code&gt; operator&lt;/h1&gt;
&lt;p&gt;Jira&apos;s JQL has a &lt;code&gt;was&lt;/code&gt; operator that checks the &lt;strong&gt;change history&lt;/strong&gt; of a field, not just its current value. So to find every issue Sarah was ever assigned to:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assignee was &quot;sarah.mitchell@example.com&quot;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple, but powerful.&lt;/p&gt;
&lt;h1&gt;Checking multiple people at once&lt;/h1&gt;
&lt;p&gt;Need to audit the work of an entire sub-team? Use &lt;code&gt;was in&lt;/code&gt; with a list:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assignee was in (&quot;sarah.mitchell@example.com&quot;, &quot;tom.bergkamp@example.com&quot;, &quot;priya.nair@example.com&quot;)
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This returns any issue that was assigned to &lt;em&gt;any&lt;/em&gt; of those three people at any point in time.&lt;/p&gt;
&lt;h1&gt;Scoping to a specific year&lt;/h1&gt;
&lt;p&gt;Combine it with the &lt;code&gt;during&lt;/code&gt; clause to limit results to a time window — useful for annual reviews, retrospectives, or handover audits:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;assignee was in (&quot;sarah.mitchell@example.com&quot;, &quot;tom.bergkamp@example.com&quot;) during (&quot;2025-01-01&quot;, &quot;2025-12-31&quot;)
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This only matches issues where one of them was the assignee &lt;em&gt;at some point during 2025&lt;/em&gt; — even if the ticket has since been reassigned or closed.&lt;/p&gt;
&lt;h1&gt;Putting it all together&lt;/h1&gt;
&lt;p&gt;A full, practical query might look like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;project = &quot;PLATFORM&quot;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;AND assignee was in (&quot;sarah.mitchell@example.com&quot;, &quot;tom.bergkamp@example.com&quot;)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;AND during (&quot;2025-01-01&quot;, &quot;2025-12-31&quot;)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;ORDER BY updated DESC
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;was&lt;/code&gt; operator works on other fields too — &lt;code&gt;status was &quot;In Progress&quot;&lt;/code&gt;, &lt;code&gt;priority was &quot;High&quot;&lt;/code&gt;, etc. — so it&apos;s worth keeping in your JQL toolkit whenever you need to query history rather than current state.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-31T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/how-to-find-all-jira-issues-ever-assigned-to-someone-even-historical-ones</id>
    <title>🐥 How to find all Jira issues ever assigned to someone (even historical ones)</title>
    <updated>2026-05-31T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/downloading-external-images-as-squares-from-a-phoenix-app" rel="alternate"/>
    <content type="html">&lt;p&gt;A common need in admin tools: click a button, download a remote image as a square JPEG. Simple enough — until CORS gets in the way.&lt;/p&gt;
&lt;h1&gt;The CORS problem&lt;/h1&gt;
&lt;p&gt;When fetching a cross-origin image and drawing it onto a &lt;code&gt;&lt;canvas&gt;&lt;/code&gt;, the browser marks the canvas as &quot;tainted&quot;. The moment you call &lt;code&gt;canvas.toBlob()&lt;/code&gt; to read the pixel data back out, it throws a security error. Unless the image server sends explicit CORS headers — which most don&apos;t — you can&apos;t do canvas operations on cross-origin images.&lt;/p&gt;
&lt;h1&gt;The fix: a server-side proxy&lt;/h1&gt;
&lt;p&gt;The solution is to proxy the image through the Phoenix app. From the browser&apos;s point of view the image comes from the same origin, so the canvas stays clean.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;image_proxy&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;url&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;allowed_url?&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;       &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Req&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;content_type&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;headers&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;image/jpeg&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;hd&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;put_resp_content_type&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;content_type&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Invalid URL&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;502&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Failed to fetch image&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;allowed_url?/1&lt;/code&gt; function validates the host against a known allowlist — an important SSRF guard so the proxy can&apos;t be abused to fetch arbitrary internal URLs.&lt;/p&gt;
&lt;h1&gt;Center-cropping to a square in the browser&lt;/h1&gt;
&lt;p&gt;Once the image loads from the proxy, a small Canvas API snippet handles the crop. The logic is straightforward: use the shorter dimension as the square size, then offset into the longer dimension by half the difference to take the center slice.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-javascript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;variable-builtin&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;phx:download-square-image&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;filename&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;detail&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;proxyUrl&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;`/image-proxy?url=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;function-builtin&quot;&gt;encodeURIComponent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method&quot;&gt;onload&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;naturalWidth&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;naturalHeight&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;canvas&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;width&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;height&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;2d&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;offsetX&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;naturalWidth&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;offsetY&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;naturalHeight&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;offsetX&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;offsetY&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;toBlob&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;      &lt;span class=&quot;keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;href&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;createObjectURL&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;download&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;      &lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;      &lt;span class=&quot;constant&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;revokeObjectURL&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;image/jpeg&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0.95&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;proxyUrl&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a landscape image the left and right edges are trimmed, keeping the center. For a portrait image the top and bottom are trimmed instead. The full shorter dimension is always preserved — no upscaling, no padding.&lt;/p&gt;
&lt;h1&gt;Wiring it up in LiveView&lt;/h1&gt;
&lt;p&gt;The download is triggered from the template using Phoenix&apos;s &lt;code&gt;JS.dispatch/2&lt;/code&gt;, which fires a custom DOM event with the image URL and filename as detail:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-heex&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;tag-attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;cursor-pointer&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;tag-attribute&quot;&gt;phx-click&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;module&quot;&gt;JS&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;phx:download-square-image&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;detail: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;image_url&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;filename: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;image.jpg&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;tag-delimiter&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  Download
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No LiveView round-trip needed — &lt;code&gt;JS.dispatch&lt;/code&gt; fires the event directly in the browser, the &lt;code&gt;window&lt;/code&gt; listener catches it, and the download happens entirely client-side after the one proxy fetch.&lt;/p&gt;
&lt;h1&gt;Why &lt;code&gt;naturalWidth&lt;/code&gt; instead of &lt;code&gt;width&lt;/code&gt;?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;img.width&lt;/code&gt; returns the CSS-rendered size, which is meaningless for an image that isn&apos;t attached to the DOM. &lt;code&gt;img.naturalWidth&lt;/code&gt; always returns the actual pixel dimensions of the image data — the right value to use when doing pixel-level canvas operations.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/javascript&quot;&gt;#javascript&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/frontend&quot;&gt;#frontend&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-29T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/downloading-external-images-as-squares-from-a-phoenix-app</id>
    <title>🐥 Downloading external images as squares from a Phoenix app</title>
    <updated>2026-05-29T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/the-tailwind-enabled-selector-trick-for-disabled-buttons" rel="alternate"/>
    <content type="html">&lt;p&gt;When you add a &lt;code&gt;disabled&lt;/code&gt; attribute to a &lt;code&gt;&lt;button&gt;&lt;/code&gt; element, you probably expect it to just... look disabled. But there&apos;s a subtle trap that catches a lot of developers: your &lt;code&gt;hover:&lt;/code&gt; styles still apply visually, even when the button is disabled.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;Consider a typical Tailwind button:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-html&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;bg-gray-200 hover:bg-gray-300 hover:cursor-pointer&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  Click me
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though the button is &lt;code&gt;disabled&lt;/code&gt; and won&apos;t fire any events, the &lt;code&gt;hover:bg-gray-300&lt;/code&gt; and &lt;code&gt;hover:cursor-pointer&lt;/code&gt; classes still apply on hover. The cursor becomes a pointer and the background changes — giving the user a false signal that the button is interactive.&lt;/p&gt;
&lt;h1&gt;The fix: &lt;code&gt;enabled:&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Tailwind ships with an &lt;code&gt;enabled:&lt;/code&gt; variant that maps directly to the CSS &lt;code&gt;:enabled&lt;/code&gt; pseudo-class. Swap your &lt;code&gt;hover:&lt;/code&gt; styles for &lt;code&gt;enabled:hover:&lt;/code&gt; and the problem disappears:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-html&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;bg-gray-200 enabled:hover:bg-gray-300 enabled:hover:cursor-pointer&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;attribute&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  Click me
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now those styles only apply when the button is &lt;strong&gt;not&lt;/strong&gt; disabled. No JavaScript, no conditional class logic, no extra wrapper — just a single variant prefix.&lt;/p&gt;
&lt;h1&gt;Why this matters&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;:enabled&lt;/code&gt; pseudo-class is the semantic opposite of &lt;code&gt;:disabled&lt;/code&gt;. It&apos;s supported in all modern browsers and has been in CSS for years, but it&apos;s easy to overlook because disabled states are often handled with opacity or a wrapper &lt;code&gt;div&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;enabled:hover:&lt;/code&gt; keeps your intent explicit in the markup and makes disabled state handling a one-liner in your component library.&lt;/p&gt;
&lt;h1&gt;In practice (Phoenix / LiveView)&lt;/h1&gt;
&lt;p&gt;If you have a reusable button component, this is the ideal place to apply it. Instead of:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;string&quot;&gt;&quot;hover:bg-gray-300 hover:cursor-pointer&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Write:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;string&quot;&gt;&quot;enabled:hover:bg-gray-300 enabled:hover:cursor-pointer&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now any caller that passes &lt;code&gt;disabled&lt;/code&gt; as an attribute gets correct visual behavior automatically — no special-case classes needed at the call site.&lt;/p&gt;
&lt;p&gt;Small trick, but it saves a prop, a conditional, and a subtle UX bug all at once.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/frontend&quot;&gt;#frontend&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/css&quot;&gt;#css&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-27T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/the-tailwind-enabled-selector-trick-for-disabled-buttons</id>
    <title>🐥 The Tailwind `enabled:` selector trick for disabled buttons</title>
    <updated>2026-05-27T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/hide-a-layout-section-from-a-specific-child-view-in-laravel-blade" rel="alternate"/>
    <content type="html">&lt;p&gt;Sometimes you have a shared layout that renders a block — a promotional banner, a sidebar widget, an image — that makes sense on most pages but not all. Rather than duplicating the layout or reaching for JavaScript, you can solve this cleanly with two built-in Blade directives: &lt;code&gt;@section&lt;/code&gt; and &lt;code&gt;View::hasSection()&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;Say your login layout renders a decorative image at the bottom of every page:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&amp;lbrace;&amp;lbrace;-- resources/views/layouts/login.blade.php --&amp;rbrace;&amp;rbrace;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;@php $loginImage = Contractify::loginImage(); @endphp
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;@if ($loginImage-&gt;src)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;div class=&quot;...&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;        &lt;img src=&quot;&amp;lbrace;&amp;lbrace; $loginImage-&gt;src &amp;rbrace;&amp;rbrace;&quot; alt=&quot;&amp;lbrace;&amp;lbrace; $loginImage-&gt;alt &amp;rbrace;&amp;rbrace;&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;/div&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;@endif
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is fine for the login and registration pages, but on the OAuth authorization page it&apos;s visual noise you&apos;d rather skip.&lt;/p&gt;
&lt;h1&gt;The solution&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Wrap the block in the layout with &lt;code&gt;@unless(View::hasSection(...))&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&amp;lbrace;&amp;lbrace;-- resources/views/layouts/login.blade.php --&amp;rbrace;&amp;rbrace;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;@unless(View::hasSection(&amp;#39;hide-login-image&amp;#39;))
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    @php $loginImage = Contractify::loginImage(); @endphp
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    @if ($loginImage-&gt;src)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;        &lt;div class=&quot;...&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;            &lt;img src=&quot;&amp;lbrace;&amp;lbrace; $loginImage-&gt;src &amp;rbrace;&amp;rbrace;&quot; alt=&quot;&amp;lbrace;&amp;lbrace; $loginImage-&gt;alt &amp;rbrace;&amp;rbrace;&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;/div&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    @endif
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;@endunless
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Declare the empty section in the child view that should opt out:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&amp;lbrace;&amp;lbrace;-- resources/views/vendor/passport/authorize.blade.php --&amp;rbrace;&amp;rbrace;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;@extends(&amp;#39;layouts.login&amp;#39;)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;@section(&amp;#39;hide-login-image&amp;#39;)@endsection
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;@section(&amp;#39;content&amp;#39;)
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &amp;lbrace;&amp;lbrace;-- ... --&amp;rbrace;&amp;rbrace;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;@endsection
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;How it works&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;View::hasSection(&apos;hide-login-image&apos;)&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; if the currently-rendering child view has declared a section with that name — even an empty one. The &lt;code&gt;@unless&lt;/code&gt; then skips the block entirely.&lt;/p&gt;
&lt;p&gt;Every other view that extends the layout leaves that section undeclared, so &lt;code&gt;hasSection&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; and the image renders as normal.&lt;/p&gt;
&lt;h1&gt;Why this is better than the alternatives&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No layout duplication.&lt;/strong&gt; You keep a single source of truth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No conditionals based on route names or controller names.&lt;/strong&gt; Those couple your layout to your routing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The opt-out lives in the view that needs it.&lt;/strong&gt; Easy to find, easy to remove.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&apos;s a tiny pattern, but it keeps your layouts clean and your child views in control of their own presentation.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/frontend&quot;&gt;#frontend&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-25T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/hide-a-layout-section-from-a-specific-child-view-in-laravel-blade</id>
    <title>🐥 Hide a layout section from a specific child view in Laravel Blade</title>
    <updated>2026-05-25T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/scanning-files-with-clamdscan-and-fdpass" rel="alternate"/>
    <content type="html">&lt;p&gt;When integrating antivirus scanning into a Linux application or upload pipeline, &lt;code&gt;clamdscan&lt;/code&gt; is usually a much better choice than &lt;code&gt;clamscan&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;While &lt;code&gt;clamscan&lt;/code&gt; starts a full standalone scan process and loads virus signatures every time, &lt;code&gt;clamdscan&lt;/code&gt; talks to the long-running &lt;code&gt;clamd&lt;/code&gt; daemon. This makes scans significantly faster and more suitable for production environments.&lt;/p&gt;
&lt;p&gt;One particularly useful option is &lt;code&gt;--fdpass&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;What does --fdpass do?&lt;/h1&gt;
&lt;p&gt;Normally, &lt;code&gt;clamd&lt;/code&gt; runs under its own user account, such as &lt;code&gt;clamav&lt;/code&gt;. This can create permission issues when scanning files owned by another user or temporary upload files.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--fdpass&lt;/code&gt; option solves this by passing the already opened file descriptor to the daemon instead of letting the daemon reopen the file itself.&lt;/p&gt;
&lt;p&gt;This is especially useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upload processing&lt;/li&gt;
&lt;li&gt;Temporary files&lt;/li&gt;
&lt;li&gt;Restricted file permissions&lt;/li&gt;
&lt;li&gt;CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Web applications&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Installing ClamAV&lt;/h1&gt;
&lt;p&gt;On Debian or Ubuntu systems:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;update&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the virus definitions:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;freshclam&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Enabling clamd on startup&lt;/h1&gt;
&lt;p&gt;To ensure the daemon automatically starts after a reboot:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the service is running:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On some distributions, the service may be named &lt;code&gt;clamd&lt;/code&gt; instead.&lt;/p&gt;
&lt;h1&gt;Verifying the socket configuration&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;clamdscan&lt;/code&gt; communicates with the daemon through a Unix socket.&lt;/p&gt;
&lt;p&gt;Check the configured socket path:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;LocalSocket&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/clamav/clamd.conf&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typical values are:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;LocalSocket /run/clamav/clamd.ctl
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;LocalSocket /var/run/clamav/clamd.ctl
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also locate the socket manually:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;find&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/run&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/var/run&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;*.ctl&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Scanning a file&lt;/h1&gt;
&lt;p&gt;To scan a file quietly:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;clamdscan&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--fdpass&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;myfile.zip&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the file is clean, the command exits with status code &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If malware is detected, the exit code becomes &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Example in a shell script:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;clamdscan&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--fdpass&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;upload.pdf&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;File is clean&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Virus detected&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why use clamdscan instead of clamscan?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;clamdscan&lt;/code&gt; has several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Much faster scanning&lt;/li&gt;
&lt;li&gt;Lower memory usage&lt;/li&gt;
&lt;li&gt;Better suited for concurrent workloads&lt;/li&gt;
&lt;li&gt;No repeated signature loading&lt;/li&gt;
&lt;li&gt;Ideal for web servers and APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For applications handling many uploads or background jobs, using &lt;code&gt;clamdscan --fdpass&lt;/code&gt; is generally the recommended setup.&lt;/p&gt;
&lt;h1&gt;Common error&lt;/h1&gt;
&lt;p&gt;If you encounter:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;ERROR: Could not connect to clamd on LocalSocket
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the daemon is usually not running.&lt;/p&gt;
&lt;p&gt;Start it manually:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then retry the scan command.&lt;/p&gt;
&lt;h1&gt;Understanding exit codes&lt;/h1&gt;
&lt;p&gt;When integrating &lt;code&gt;clamdscan&lt;/code&gt; into an application or upload pipeline, it is important to distinguish between an actual virus detection and a technical failure.&lt;/p&gt;
&lt;p&gt;Fortunately, &lt;code&gt;clamdscan&lt;/code&gt; provides clear exit codes for this.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No virus found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Virus found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scanner error or operational failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This means you should avoid parsing stdout or stderr to determine the scan result. Instead, rely on the process exit code directly.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;clamdscan&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--fdpass&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;upload.pdf&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;variable&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;?&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;in&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;File is clean&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;punctuation-delimiter&quot;&gt;;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Virus detected&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;punctuation-delimiter&quot;&gt;;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Scan failed&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;        &lt;span class=&quot;punctuation-delimiter&quot;&gt;;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;esac&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This distinction is especially important in automated systems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Exit code &lt;code&gt;1&lt;/code&gt; means the scanner worked correctly and detected malware&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Exit code &lt;code&gt;2&lt;/code&gt; means something operational failed, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clamd&lt;/code&gt; not running&lt;/li&gt;
&lt;li&gt;socket connection failures&lt;/li&gt;
&lt;li&gt;permission issues&lt;/li&gt;
&lt;li&gt;corrupted scanner configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In production systems, a virus detection is usually a business-level validation failure, while exit code &lt;code&gt;2&lt;/code&gt; should typically trigger retries, logging, monitoring, or alerts.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sysadmin&quot;&gt;#sysadmin&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-23T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/scanning-files-with-clamdscan-and-fdpass</id>
    <title>🐥 Scanning files with clamdscan and --fdpass</title>
    <updated>2026-05-23T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/typescripts-pick-utility-type-select-what-you-need" rel="alternate"/>
    <content type="html">&lt;p&gt;TypeScript ships with a set of built-in utility types that make working with existing types far more ergonomic. One of the most useful is &lt;code&gt;Pick&lt;Type, Keys&gt;&lt;/code&gt; — a simple but powerful tool for carving out a subset of properties from a larger type.&lt;/p&gt;
&lt;h1&gt;What Is &lt;code&gt;Pick&lt;/code&gt;?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Pick&lt;Type, Keys&gt;&lt;/code&gt; constructs a new type by selecting a specific set of properties (&lt;code&gt;Keys&lt;/code&gt;) from an existing type (&lt;code&gt;Type&lt;/code&gt;). Think of it as a projection: you have a big object shape, and you only want a slice of it.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;K&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;P&lt;/span&gt; &lt;span class=&quot;keyword-operator&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;P&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;A Practical Example&lt;/h1&gt;
&lt;p&gt;Suppose you have a &lt;code&gt;User&lt;/code&gt; type across your application:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;passwordHash&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;createdAt&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;admin&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;editor&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;viewer&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When building a public-facing API response, you never want to expose &lt;code&gt;passwordHash&lt;/code&gt;. And a user profile card in the UI might only care about &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;email&lt;/code&gt;. Instead of duplicating the shape or manually re-declaring those fields, reach for &lt;code&gt;Pick&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;PublicUser&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Equivalent to:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// type PublicUser = &amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;comment&quot;&gt;//   id: number;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;comment&quot;&gt;//   name: string;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;comment&quot;&gt;//   email: string;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// &amp;rbrace;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;PublicUser&lt;/code&gt; stays in sync with &lt;code&gt;User&lt;/code&gt; automatically. If you rename &lt;code&gt;name&lt;/code&gt; to &lt;code&gt;fullName&lt;/code&gt; on &lt;code&gt;User&lt;/code&gt;, TypeScript will immediately flag any &lt;code&gt;Pick&lt;/code&gt; references to &lt;code&gt;&apos;name&apos;&lt;/code&gt; as an error.&lt;/p&gt;
&lt;h1&gt;Real-World Use Cases&lt;/h1&gt;
&lt;h2&gt;Form State&lt;/h2&gt;
&lt;p&gt;When a form only edits a subset of a model&apos;s fields, &lt;code&gt;Pick&lt;/code&gt; keeps the shape accurate without duplication:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;UpdateProfileForm&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;submitProfileUpdate&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;UpdateProfileForm&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;api&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;/profile&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Component Props&lt;/h2&gt;
&lt;p&gt;Vue and React components often need just a few fields from a larger model. &lt;code&gt;Pick&lt;/code&gt; makes this explicit in the prop type:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;AvatarProps&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;variable-member&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;sm&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;md&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;lg&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Service Layer Boundaries&lt;/h2&gt;
&lt;p&gt;When passing data between services, &lt;code&gt;Pick&lt;/code&gt; enforces that only the relevant fields cross the boundary — a lightweight form of data encapsulation:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;EmailPayload&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;sendWelcomeEmail&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;EmailPayload&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;variable&quot;&gt;mailer&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;variable-member&quot;&gt;subject&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;`Welcome, &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable-member&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;!`&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Combining with Other Utility Types&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Pick&lt;/code&gt; composes cleanly with other utilities. For example, &lt;code&gt;Partial&lt;Pick&lt;...&gt;&gt;&lt;/code&gt; gives you an optional subset — perfect for PATCH request bodies:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;PatchUserRequest&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Partial&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;role&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;code&gt;Pick&lt;/code&gt; vs &lt;code&gt;Omit&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Pick&lt;/code&gt; and &lt;code&gt;Omit&lt;/code&gt; are mirror images of each other:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Utility&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best when…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pick&lt;Type, Keys&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allowlist — name what you want&lt;/td&gt;
&lt;td&gt;The desired set is small&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Omit&lt;Type, Keys&gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Denylist — name what you don&apos;t want&lt;/td&gt;
&lt;td&gt;The excluded set is small&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you want all fields except one or two, &lt;code&gt;Omit&lt;/code&gt; is more concise. If you want just a handful of fields from a large type, &lt;code&gt;Pick&lt;/code&gt; is cleaner.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-typescript&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// These are equivalent when User has exactly these five fields:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;WithoutPassword&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Omit&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;passwordHash&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;SafeFields&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Pick&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;id&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;email&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;createdAt&amp;#39;&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;role&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// Prefer Omit here — fewer keys to list&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Key Takeaways&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Pick&lt;Type, Keys&gt;&lt;/code&gt; creates a new type with only the named properties from &lt;code&gt;Type&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Keys&lt;/code&gt; argument is validated against &lt;code&gt;keyof Type&lt;/code&gt; at compile time — typos are caught immediately.&lt;/li&gt;
&lt;li&gt;It keeps derived types in sync with their source; renames and removals surface as errors rather than silent mismatches.&lt;/li&gt;
&lt;li&gt;It composes well with &lt;code&gt;Partial&lt;/code&gt;, &lt;code&gt;Required&lt;/code&gt;, &lt;code&gt;Readonly&lt;/code&gt;, and &lt;code&gt;Omit&lt;/code&gt; for expressive, minimal type definitions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whenever you find yourself re-declaring a handful of fields that already exist on another type, &lt;code&gt;Pick&lt;/code&gt; is almost certainly the right tool.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/typescript&quot;&gt;#typescript&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/frontend&quot;&gt;#frontend&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-21T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/typescripts-pick-utility-type-select-what-you-need</id>
    <title>🐥 TypeScript&apos;s Pick utility type: select what you need</title>
    <updated>2026-05-21T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/color-code-your-github-pr-list-with-custom-css" rel="alternate"/>
    <content type="html">&lt;p&gt;If you spend a lot of time reviewing pull requests on GitHub, you&apos;ve probably wished the PR list gave you more visual signal at a glance. Which PRs are approved and ready to merge? Which ones have a reviewer blocking them? Which are still drafts?&lt;/p&gt;
&lt;p&gt;With the &lt;a href=&quot;https://github.com/refined-github/refined-github&quot;&gt;Refined GitHub&lt;/a&gt; browser extension and a few lines of custom CSS, you can color-code your PR list to answer all of those questions instantly.&lt;/p&gt;
&lt;h1&gt;What You&apos;ll Need&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/refined-github/refined-github&quot;&gt;Refined GitHub&lt;/a&gt; — a browser extension for Chrome, Firefox, and Safari that enhances the GitHub UI with dozens of quality-of-life improvements. One of its lesser-known features is the ability to inject your own custom CSS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Setting Up Custom CSS in Refined GitHub&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;Install Refined GitHub from your browser&apos;s extension store.&lt;/li&gt;
&lt;li&gt;Click the Refined GitHub icon in your toolbar and open its &lt;strong&gt;Options&lt;/strong&gt; page.&lt;/li&gt;
&lt;li&gt;Scroll down to the &lt;strong&gt;Custom CSS&lt;/strong&gt; section.&lt;/li&gt;
&lt;li&gt;Paste the CSS below and save.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;The CSS&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-css&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/* Draft PRs — targets the draft octicon on the PR status icon */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;js-issue-row&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;tag&quot;&gt;svg&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;octicon-git-pull-request-draft&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;property&quot;&gt;border-right&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; solid &lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;e68934&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;230&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;137&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;52&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number-float&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/* At least one approving review */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;js-issue-row&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;tag-attribute&quot;&gt;aria-label&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;*=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;review approval&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;property&quot;&gt;border-right&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; solid &lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;5eb4a7&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;94&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;180&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;167&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number-float&quot;&gt;0.2&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/* Changes Requested */&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;js-issue-row&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;tag-attribute&quot;&gt;aria-label&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;*=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;review requesting changes&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;js-issue-row&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;tag-attribute&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;Changes requested&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;property&quot;&gt;border-right&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; solid &lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;9b59b6&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;rgba&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;155&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;89&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;182&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;number-float&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;!important&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;How It Works&lt;/h1&gt;
&lt;p&gt;GitHub&apos;s PR list uses SVG status icons and review-state badges to indicate a PR&apos;s current state. The CSS uses the &lt;code&gt;:has()&lt;/code&gt; selector to look up from those indicators to the parent row and apply a colored right border and tinted background.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Selector&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;svg.octicon-git-pull-request-draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Draft PR&lt;/td&gt;
&lt;td&gt;Orange &lt;code&gt;#e68934&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[aria-label*=&quot;review approval&quot;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Approved&lt;/td&gt;
&lt;td&gt;Teal &lt;code&gt;#5eb4a7&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[aria-label*=&quot;review requesting changes&quot;]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changes requested&lt;/td&gt;
&lt;td&gt;Purple &lt;code&gt;#9b59b6&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;The Result&lt;/h1&gt;
&lt;p&gt;Head to any GitHub PR list and you&apos;ll immediately see rows highlighted by status. Approved PRs glow teal, blocked ones turn purple, and drafts stand out in orange — no more scanning each row individually.&lt;/p&gt;
&lt;p&gt;It&apos;s a small change, but when you&apos;re triaging dozens of open PRs every day, the visual grouping saves a surprising amount of time.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/css&quot;&gt;#css&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/github&quot;&gt;#github&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-19T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/color-code-your-github-pr-list-with-custom-css</id>
    <title>🐥 Color-code your GitHub PR list with custom CSS</title>
    <updated>2026-05-19T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/why-dexter-lsp-doesnt-autocomplete-elixir-stdlib-modules" rel="alternate"/>
    <content type="html">&lt;p&gt;If you&apos;re using &lt;a href=&quot;https://github.com/elixir-tools/dexter&quot;&gt;Dexter&lt;/a&gt; as your Elixir LSP in VS Code and wondering why &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Enum&lt;/code&gt;, and other standard library modules don&apos;t show up in autocomplete — the answer is likely a missing or misconfigured &lt;code&gt;stdlibPath&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;The Problem&lt;/h1&gt;
&lt;p&gt;Dexter needs access to the &lt;strong&gt;Elixir source files&lt;/strong&gt; (&lt;code&gt;.ex&lt;/code&gt;) to index the stdlib. When you install Elixir via Homebrew, only compiled &lt;code&gt;.beam&lt;/code&gt; files are included — no source. So even if you point &lt;code&gt;dexter.stdlibPath&lt;/code&gt; at something like:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-json&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;&quot;dexter.stdlibPath&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; &quot;/opt/homebrew/opt/elixir/lib/elixir/lib&quot;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...there&apos;s nothing for Dexter to index, and stdlib autocomplete silently doesn&apos;t work.&lt;/p&gt;
&lt;h1&gt;The Fix&lt;/h1&gt;
&lt;p&gt;Clone the Elixir source at the version matching your runtime:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;elixir&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--version&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clone&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;https://github.com/elixir-lang/elixir.git&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;~/elixir-src&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--depth=1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--branch&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;v1.18.3&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then update your &lt;code&gt;.vscode/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-json&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;&quot;dexter.stdlibPath&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; &quot;/Users/yourname/elixir-src/lib&quot;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;lib/&lt;/code&gt; directory contains subdirectories like &lt;code&gt;elixir/lib/&lt;/code&gt;, &lt;code&gt;mix/lib/&lt;/code&gt;, &lt;code&gt;logger/lib/&lt;/code&gt;, etc. — all with proper &lt;code&gt;.ex&lt;/code&gt; source files that Dexter can index for autocomplete.&lt;/p&gt;
&lt;p&gt;After restarting VS Code, stdlib completions for &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Enum&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;, and friends should start appearing.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/vscode&quot;&gt;#vscode&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-17T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/why-dexter-lsp-doesnt-autocomplete-elixir-stdlib-modules</id>
    <title>🐥 Why Dexter LSP doesn&apos;t autocomplete Elixir stdlib modules</title>
    <updated>2026-05-17T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/keeping-elixir-stdlib-source-in-sync-with-your-project" rel="alternate"/>
    <content type="html">&lt;p&gt;When working with tools like &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=dexter.dexter&quot;&gt;Dexter&lt;/a&gt; for Elixir code intelligence in VS Code, it&apos;s useful to have the Elixir standard library source checked out locally. Dexter uses it to provide go-to-definition and hover docs for stdlib modules.&lt;/p&gt;
&lt;p&gt;The naive approach — cloning it once by hand — breaks down the moment you upgrade Elixir. You forget to re-clone, the path is wrong, and suddenly your editor is jumping to source that doesn&apos;t match what&apos;s actually running.&lt;/p&gt;
&lt;p&gt;Here&apos;s a small shell script that automates it.&lt;/p&gt;
&lt;h1&gt;The script&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-directive&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-euo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;pipefail&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;variable&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;elixir&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--version&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-oE&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;Elixir [0-9]+\.[0-9]+\.[0-9]+&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;-1&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;&amp;lbrace;print $2&amp;rbrace;&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Error: could not determine Elixir version&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;2&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;function-builtin&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;v&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-builtin&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;/.elixir_stdlib&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Elixir version: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-C&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--points-at&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;HEAD&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;string-special-path&quot;&gt;/dev/null&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-qx&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; already at &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;, skipping.&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;  &lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;  &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Found &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; at a different version, removing for &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; ...&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-rf&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;&lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Cloning branch &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; into &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; ...&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;advice.detachedHead=false&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clone&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;https://github.com/elixir-lang/elixir.git&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--depth=1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--branch&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What it does&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Detects the running Elixir version.&lt;/strong&gt; It calls &lt;code&gt;elixir --version&lt;/code&gt; and extracts the version number, so there&apos;s no hardcoded version to keep in sync manually.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clones into &lt;code&gt;.elixir_stdlib&lt;/code&gt; next to the script.&lt;/strong&gt; Using &lt;code&gt;dirname &quot;$0&quot;&lt;/code&gt; makes the destination relative to the script itself, so it works regardless of your current working directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skips the clone if already up to date.&lt;/strong&gt; Before doing anything destructive, it checks whether the target tag (&lt;code&gt;v1.19.5&lt;/code&gt;, for example) is among the tags pointing at &lt;code&gt;HEAD&lt;/code&gt; in the existing clone. This matters because the Elixir repo uses floating tags like &lt;code&gt;v1.19-latest&lt;/code&gt; that point at the same commit — a naive &lt;code&gt;git describe&lt;/code&gt; would return that tag instead, causing a false version mismatch every time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wipes and re-clones on version change.&lt;/strong&gt; If the installed Elixir version doesn&apos;t match what&apos;s checked out, the old folder is removed and the correct version is cloned fresh.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Suppresses the detached HEAD warning.&lt;/strong&gt; Cloning a tag always results in a detached HEAD, which git warns about by default. Passing &lt;code&gt;-c advice.detachedHead=false&lt;/code&gt; silences that noise without hiding anything useful.&lt;/p&gt;
&lt;h1&gt;Usage&lt;/h1&gt;
&lt;p&gt;Drop the script in your project root, make it executable, add &lt;code&gt;.elixir_stdlib&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;, and point your editor at &lt;code&gt;$&amp;lbrace;workspaceFolder&amp;rbrace;/.elixir_stdlib/lib&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;+x&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;clone-elixir-src.sh&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;./clone-elixir-src.sh&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it again whenever you upgrade Elixir — it&apos;ll figure out the rest.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/vscode&quot;&gt;#vscode&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-15T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/keeping-elixir-stdlib-source-in-sync-with-your-project</id>
    <title>🐥 Keeping Elixir stdlib source in sync with your project</title>
    <updated>2026-05-15T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/anti-corruption-layer-in-elixir-phoenix-keep-your-domain-clean" rel="alternate"/>
    <content type="html">&lt;p&gt;When you integrate an external service — a payment provider, a shipping API, a CRM — you face a quiet but persistent risk: the external system&apos;s model starts leaking into your own. Status strings, provider-specific IDs, and SDK structs end up scattered across your business logic. Changing providers later means touching code you should never have had to change.&lt;/p&gt;
&lt;p&gt;The Anti-Corruption Layer (ACL) is a pattern from Domain-Driven Design that puts an explicit translation boundary between your domain and the external world. Everything provider-specific lives in one place. Your domain never knows it exists.&lt;/p&gt;
&lt;p&gt;This post walks through implementing it in Elixir and Phoenix, using a payment integration as the running example.&lt;/p&gt;
&lt;h1&gt;The Problem&lt;/h1&gt;
&lt;p&gt;Suppose you&apos;re integrating Stripe directly. Without a boundary, it&apos;s easy to write something like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyApp.Orders&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;complete_order&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Req&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;https://api.stripe.com/v1/payment_intents&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;           &lt;span class=&quot;string-special-symbol&quot;&gt;auth: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:basic&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;stripe_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;           &lt;span class=&quot;string-special-symbol&quot;&gt;form: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;amount: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;total_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;eur&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;confirm: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;         &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;body: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;status&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;succeeded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;module&quot;&gt;Orders&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;mark_paid&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;body: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;error&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;message&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works on day one. But now your &lt;code&gt;Orders&lt;/code&gt; context knows that payments have a &lt;code&gt;&quot;succeeded&quot;&lt;/code&gt; status string, that IDs come from &lt;code&gt;intent[&quot;id&quot;]&lt;/code&gt;, and that Stripe&apos;s API lives at that specific URL. If you ever swap providers — or just want to test without hitting Stripe — you have to unpick all of this from business logic.&lt;/p&gt;
&lt;h1&gt;Core Concepts in Elixir&lt;/h1&gt;
&lt;p&gt;The ACL pattern maps cleanly to Elixir idioms:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DDD concept&lt;/th&gt;
&lt;th&gt;Elixir equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Domain model&lt;/td&gt;
&lt;td&gt;Plain &lt;code&gt;defstruct&lt;/code&gt; (not an Ecto schema)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port / interface&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;@behaviour&lt;/code&gt; module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACL implementation&lt;/td&gt;
&lt;td&gt;A module implementing the behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain service&lt;/td&gt;
&lt;td&gt;A context module that only calls the behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wiring&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Application.get_env/2&lt;/code&gt; and config files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Step 1: Define Your Domain Structs&lt;/h1&gt;
&lt;p&gt;These live in your domain and use your language — not Stripe&apos;s.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;type &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:pending&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:captured&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:declined&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:refunded&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;enforce_keys &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:status&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:currency&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defstruct&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:status&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:currency&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentEvent&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;type &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_captured&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_declined&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_refunded&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;enforce_keys &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:type&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defstruct&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:type&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No Stripe types, no raw maps, no magic strings. If you switch providers, these structs stay exactly as they are.&lt;/p&gt;
&lt;h1&gt;Step 2: Define the Port Behaviour&lt;/h1&gt;
&lt;p&gt;The behaviour declares what the domain needs — a contract, not an implementation.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentGateway&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;type &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;charge_result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;type &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund_result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;callback &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;              &lt;span class=&quot;function-call&quot;&gt;charge_result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;callback &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;refund_result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The domain context will call through this behaviour. It never imports Stripe modules or touches raw HTTP responses.&lt;/p&gt;
&lt;h1&gt;Step 3: Implement the ACL&lt;/h1&gt;
&lt;p&gt;This is the only module allowed to know anything about Stripe. All HTTP calls and all translation logic live here.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.StripeGateway&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;behaviour &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentGateway&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;base_url &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;https://api.stripe.com/v1&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;module&quot;&gt;Req&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;post!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;/payment_intents&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;string-special-symbol&quot;&gt;auth: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:basic&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;string-special-symbol&quot;&gt;form: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;amount: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;payment_method: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;confirm: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;&quot;automatic_payment_methods[enabled]&quot;: &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;          &lt;span class=&quot;string-special-symbol&quot;&gt;&quot;automatic_payment_methods[allow_redirects]&quot;: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;never&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;      &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;translate_intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;      &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;      &lt;span class=&quot;module&quot;&gt;Req&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;post!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;/refunds&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;        &lt;span class=&quot;string-special-symbol&quot;&gt;auth: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:basic&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;        &lt;span class=&quot;string-special-symbol&quot;&gt;form: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;payment_intent: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;34&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;35&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;36&quot;&gt;      &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;translate_refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;37&quot;&gt;      &lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;38&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;39&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;40&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;41&quot;&gt;  &lt;span class=&quot;comment&quot;&gt;# --- Translation layer ---&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;42&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;43&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;44&quot;&gt;    &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;45&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;46&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;47&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;48&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;49&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;50&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;51&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;52&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;53&quot;&gt;    &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;54&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;payment_intent&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;55&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:refunded&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;56&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;57&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;58&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;59&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;60&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;61&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;succeeded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:captured&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;62&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;processing&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:pending&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;63&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;requires_payment_method&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:declined&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;64&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;canceled&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:declined&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;65&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_status&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:pending&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;66&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;67&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;fetch_env!&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:stripe_secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;68&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pattern matching on &lt;code&gt;translate_status/1&lt;/code&gt; makes the translation explicit and exhaustive. Adding a new Stripe status is a one-line change in exactly one place.&lt;/p&gt;
&lt;h1&gt;Step 4: Write a Clean Domain Context&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;Billing&lt;/code&gt; context only speaks the behaviour. It has no idea which gateway is wired up.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;process_payment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;gateway&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;issue_refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;function-call&quot;&gt;gateway&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;gateway&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;module&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get_env&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_gateway&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.StripeGateway&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing in here ties you to Stripe. The context is pure business logic.&lt;/p&gt;
&lt;h1&gt;Step 5: Wire It Up in Config&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# config/config.exs&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_gateway&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.StripeGateway&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:stripe_secret_key&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;get_env&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;STRIPE_SECRET_KEY&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# config/test.exs&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;config&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:payment_gateway&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.FakeGateway&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Switching providers in production means changing one config line and shipping a new gateway module. Nothing else changes.&lt;/p&gt;
&lt;h1&gt;Handling Webhooks&lt;/h1&gt;
&lt;p&gt;Webhooks are where external models are most tempting to let through. A Phoenix controller should translate immediately and dispatch a domain event — it should not pass raw Stripe payloads any further.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb.StripeWebhookController&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;MyAppWeb&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:controller&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;translate_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;module&quot;&gt;Billing&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;handle_payment_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:unhandled&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;      &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;         &lt;span class=&quot;string&quot;&gt;&quot;type&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;payment_intent.succeeded&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;         &lt;span class=&quot;string&quot;&gt;&quot;data&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;object&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;id&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;amount&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;amount&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;       &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentEvent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:payment_captured&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;payment_id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;         &lt;span class=&quot;string&quot;&gt;&quot;type&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;payment_intent.payment_failed&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;         &lt;span class=&quot;string&quot;&gt;&quot;data&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;object&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;id&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;amount&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;amount&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;28&quot;&gt;       &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;29&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentEvent&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:payment_declined&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;payment_id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;30&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;31&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;32&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;translate_event&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string-special-symbol&quot;&gt;:unhandled&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;33&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The controller is the ACL for inbound Stripe events. Once translated, &lt;code&gt;Billing.handle_payment_event/1&lt;/code&gt; receives only &lt;code&gt;%Billing.PaymentEvent&amp;lbrace;&amp;rbrace;&lt;/code&gt; structs — never raw Stripe data.&lt;/p&gt;
&lt;h1&gt;Testing Without a Real Gateway&lt;/h1&gt;
&lt;p&gt;Because the gateway is a behaviour, you can swap in a fake implementation for tests without mocks or HTTP stubs.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;Billing.Gateways.FakeGateway&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;behaviour &lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentGateway&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;_amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;fail_card&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Card declined&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;charge&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;comment&quot;&gt;_source&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;fake_&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;#&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;unique_integer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:positive&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;string-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:captured&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;amount_cents&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;currency&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;  &lt;span class=&quot;constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;impl &lt;/span&gt;&lt;span class=&quot;boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;  &lt;span class=&quot;keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;refund&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;payment_id&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:refunded&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;amount_cents: &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;      &lt;span class=&quot;string-special-symbol&quot;&gt;currency: &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;eur&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;26&quot;&gt;  &lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;27&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your tests call &lt;code&gt;Billing.process_payment/3&lt;/code&gt; exactly as production code does — there is no test-only branch in the domain logic, and no HTTP traffic.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;successful payment returns a captured result&quot;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Billing.PaymentResult&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:captured&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;           &lt;span class=&quot;module&quot;&gt;Billing&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;process_payment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1999&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;eur&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;tok_visa&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;declined card returns an error&quot;&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;string-special-symbol&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Card declined&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;           &lt;span class=&quot;module&quot;&gt;Billing&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;process_payment&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1999&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;eur&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;fail_card&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Elixir-Specific Considerations&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Behaviours vs. protocols.&lt;/strong&gt; Use a &lt;code&gt;@behaviour&lt;/code&gt; when you have multiple implementations of the same concept (payment gateways, SMS providers). Use a &lt;code&gt;protocol&lt;/code&gt; when you want polymorphism across data types you don&apos;t control. For the ACL pattern, behaviours are the right tool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pattern matching is your translation engine.&lt;/strong&gt; The &lt;code&gt;translate_status/1&lt;/code&gt; clauses above are cleaner than a &lt;code&gt;case&lt;/code&gt; block or a map lookup. If Stripe adds a new status you haven&apos;t handled, the &lt;code&gt;_&lt;/code&gt; clause captures it safely, and you can add a specific clause when you care about it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep Ecto schemas out of the ACL.&lt;/strong&gt; It&apos;s tempting to map directly from a Stripe response onto an Ecto schema. Resist this. Plain structs are easier to reason about, test, and evolve independently from your database shape. Persist only after your domain logic has run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stateful gateways.&lt;/strong&gt; If your gateway implementation needs its own process — say, to maintain an authenticated session or a connection pool — start it in your application&apos;s supervision tree. The behaviour contract stays the same; the implementation just has a &lt;code&gt;start_link/1&lt;/code&gt; that gets wired into the supervisor.&lt;/p&gt;
&lt;h1&gt;Common Pitfalls&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Returning raw maps from the gateway.&lt;/strong&gt; If &lt;code&gt;charge/3&lt;/code&gt; returns &lt;code&gt;%&amp;lbrace;&quot;id&quot; =&gt; ..., &quot;status&quot; =&gt; &quot;succeeded&quot;&amp;rbrace;&lt;/code&gt;, you&apos;ve moved the contamination rather than removed it. Always return your own structs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Putting translation in the Phoenix controller for everything.&lt;/strong&gt; The controller is the right place for webhook translation (it&apos;s the entry point). But if you&apos;re doing translation inside action handlers for outgoing calls, extract it into the gateway module where it belongs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One ACL module for all external systems.&lt;/strong&gt; If you add a shipping provider, create &lt;code&gt;Shipping.Gateways.DHLGateway&lt;/code&gt; with its own behaviour. Don&apos;t grow &lt;code&gt;StripeGateway&lt;/code&gt; into a general-purpose external-systems module.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Letting provider IDs become first-class domain concepts.&lt;/strong&gt; Storing a Stripe payment intent ID as &lt;code&gt;billing_stripe_id&lt;/code&gt; in your domain is a smell — the domain now knows about Stripe. Store it as &lt;code&gt;external_payment_id&lt;/code&gt; or in a separate gateway-specific record that your ACL manages.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Elixir&apos;s behaviours give you a clean, compiler-checked way to define the boundary between your domain and the outside world. Pattern matching makes the translation logic readable and exhaustive. Swapping configs gives you test isolation without mocks.&lt;/p&gt;
&lt;p&gt;The ACL pattern keeps your core business logic stable regardless of what external APIs do. When Stripe changes a status code or you decide to try a different provider, you update one module. Your domain, your tests, and everything that depends on them stays untouched.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/pattern&quot;&gt;#pattern&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/elixir&quot;&gt;#elixir&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/phoenix&quot;&gt;#phoenix&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-13T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/anti-corruption-layer-in-elixir-phoenix-keep-your-domain-clean</id>
    <title>🐥 Anti-Corruption Layer in Elixir/Phoenix - Keep your domain clean</title>
    <updated>2026-05-13T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/restarting-supervisord-daemons-by-working-directory-on-linux" rel="alternate"/>
    <content type="html">&lt;p&gt;When you run multiple instances of the same Laravel Horizon worker (or any supervisord-managed process) across different deployment directories, you&apos;ll quickly notice that supervisord assigns generic auto-generated names like &lt;code&gt;daemon-282661:daemon-282661_00&lt;/code&gt;. There&apos;s no obvious way to tell which daemon belongs to which application directory — until you know the trick.&lt;/p&gt;
&lt;h1&gt;Finding the working directory of a process&lt;/h1&gt;
&lt;p&gt;On Linux, every running process exposes its current working directory as a symlink under &lt;code&gt;/proc&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;readlink&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/proc/&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;string-special-path&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;string-special-path&quot;&gt;/cwd&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To check all your Horizon workers at once, grab their PIDs from &lt;code&gt;supervisorctl status&lt;/code&gt; and loop over them:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;91286&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;91287&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;91288&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;91290&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;readlink&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/proc/&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;/cwd&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;done&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Restarting daemons for a specific directory&lt;/h1&gt;
&lt;p&gt;Once you can map PIDs to directories, it&apos;s straightforward to build a script that restarts only the daemons running from a given path:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-directive&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;constant&quot;&gt;TARGET_DIR&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:?&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;Usage:&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; &lt;working-directory&gt;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;constant&quot;&gt;TARGET_DIR&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;realpath&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;TARGET_DIR&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;supervisorctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-repeat&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;RUNNING&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;continue&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;&amp;lbrace;print $1&amp;rbrace;&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-oP&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;pid \K[0-9]+&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;function-builtin&quot;&gt;continue&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;cwd&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;readlink&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;/proc/&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;/cwd&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;string-special-path&quot;&gt;/dev/null&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;cwd&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;TARGET_DIR&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;keyword-conditional&quot;&gt;then&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Restarting &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;string&quot;&gt; (pid &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;, dir &lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;cwd&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;)&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;function-call&quot;&gt;supervisorctl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;restart&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;keyword-conditional&quot;&gt;fi&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;&lt;span class=&quot;keyword-repeat&quot;&gt;done&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;How it works&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;supervisorctl status&lt;/code&gt; lists all daemons with their current PIDs.&lt;/li&gt;
&lt;li&gt;Non-&lt;code&gt;RUNNING&lt;/code&gt; daemons are skipped (they have no PID to look up).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/proc/&lt;pid&gt;/cwd&lt;/code&gt; resolves the actual working directory for each process.&lt;/li&gt;
&lt;li&gt;Any daemon whose working directory matches the target path (or is a subpath of the target path) gets restarted.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Usage&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;+x&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;restart-daemons.sh&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;./restart-daemons.sh&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/home/forge/my-app/current&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is especially useful on servers running multiple deployments of the same application — for example, a staging environment with both a &lt;code&gt;current&lt;/code&gt; and a &lt;code&gt;previous&lt;/code&gt; release still running workers.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sysadmin&quot;&gt;#sysadmin&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-11T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/restarting-supervisord-daemons-by-working-directory-on-linux</id>
    <title>🐥 Restarting supervisord daemons by working directory on Linux</title>
    <updated>2026-05-11T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/make-your-shell-scripts-environment-variables-overridable" rel="alternate"/>
    <content type="html">&lt;p&gt;When writing shell scripts, hardcoded values are a common source of friction. A script that works perfectly on your machine may need adjustments on a colleague&apos;s setup, in CI, or in production — and forcing people to edit the script itself is messy.&lt;/p&gt;
&lt;p&gt;There&apos;s a simple Bash idiom that solves this cleanly.&lt;/p&gt;
&lt;h1&gt;The Problem&lt;/h1&gt;
&lt;p&gt;Consider a typical startup script:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;8888&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;0.0.0.0&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;uvicorn&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;myapp:app&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The port is hardcoded. If you need to run two instances, or if &lt;code&gt;8888&lt;/code&gt; is already taken, you have to edit the file.&lt;/p&gt;
&lt;h1&gt;The Fix: &lt;code&gt;$&amp;lbrace;VAR:-default&amp;rbrace;&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Replace the hardcoded assignment with a default-value expansion:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;8888&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;0.0.0.0&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;uvicorn&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;myapp:app&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The syntax &lt;code&gt;$&amp;lbrace;VAR:-default&amp;rbrace;&lt;/code&gt; means:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use the value of &lt;code&gt;$VAR&lt;/code&gt; if it is set and non-empty; otherwise use &lt;code&gt;default&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The script&apos;s behaviour is unchanged when no environment variable is provided — the default kicks in. But now callers can override it without touching the file:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;9000&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;./bin/start.sh&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or by exporting it beforehand:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;9000&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;./bin/start.sh&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why This Matters&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No edits required.&lt;/strong&gt; The script stays clean; overrides happen at call time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI-friendly.&lt;/strong&gt; Most CI systems let you set environment variables per job. Your script picks them up automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-documenting.&lt;/strong&gt; The default value lives right next to the variable name, so readers immediately know what to expect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable.&lt;/strong&gt; You can layer overrides: a &lt;code&gt;.env&lt;/code&gt; file, a CI variable, or a one-liner prefix — whatever fits the context.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Variants Worth Knowing&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;lbrace;VAR:-default&amp;rbrace;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;default&lt;/code&gt; if &lt;code&gt;VAR&lt;/code&gt; is unset &lt;strong&gt;or empty&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;lbrace;VAR-default&amp;rbrace;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;default&lt;/code&gt; only if &lt;code&gt;VAR&lt;/code&gt; is unset (empty string is kept)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;lbrace;VAR:=default&amp;rbrace;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;default&lt;/code&gt; and &lt;strong&gt;assign it back&lt;/strong&gt; to &lt;code&gt;VAR&lt;/code&gt; if unset or empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&amp;lbrace;VAR:?message&amp;rbrace;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Abort with &lt;code&gt;message&lt;/code&gt; if &lt;code&gt;VAR&lt;/code&gt; is unset or empty&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For most cases, &lt;code&gt;$&amp;lbrace;VAR:-default&amp;rbrace;&lt;/code&gt; is the right choice. The &lt;code&gt;:=&lt;/code&gt; variant is useful when you want the variable available for the rest of the script without repeating the default.&lt;/p&gt;
&lt;h1&gt;A Real-World Example&lt;/h1&gt;
&lt;p&gt;Here&apos;s a before/after for a uvicorn startup script:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;0.0.0.0&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;8888&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;uvicorn&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;docai.api.app:app&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;0.0.0.0&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;$&amp;lbrace;&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;:-&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;8888&lt;/span&gt;&lt;span class=&quot;punctuation-special&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;uvicorn&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;docai.api.app:app&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;HOST&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--port&lt;/span&gt; &lt;span class=&quot;punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;PORT&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two characters added per line (&lt;code&gt;$&amp;lbrace;&lt;/code&gt; and &lt;code&gt;:-&lt;/code&gt;), zero behaviour change by default, and now fully overridable from the outside. A small habit with outsized payoff.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/terminal&quot;&gt;#terminal&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-09T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/make-your-shell-scripts-environment-variables-overridable</id>
    <title>🐥 Make your shell scripts&apos; environment variables overridable</title>
    <updated>2026-05-09T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/using-generators-for-phpunit-data-providers" rel="alternate"/>
    <content type="html">&lt;p&gt;When writing PHPUnit tests, data providers are one of the simplest ways to increase coverage without duplicating test logic. Most examples use arrays, but PHP generators (&lt;code&gt;yield&lt;/code&gt;) are often a better fit: they’re more expressive, memory-efficient, and easier to extend.&lt;/p&gt;
&lt;p&gt;This post walks through a clean, generic approach to using generators for PHPUnit data providers.&lt;/p&gt;
&lt;h1&gt;Why use generators instead of arrays?&lt;/h1&gt;
&lt;p&gt;A traditional data provider might look like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;provideCases&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;array&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;case A&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;input-a&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;expected-a&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;string&quot;&gt;&amp;#39;case B&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;input-b&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;expected-b&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works, but generators give you a few advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No need to build a full array in memory&lt;/li&gt;
&lt;li&gt;Named cases are more natural with &lt;code&gt;yield&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Easier to compose or split into smaller logical chunks&lt;/li&gt;
&lt;li&gt;Better readability when cases grow&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;A simple example&lt;/h1&gt;
&lt;p&gt;Imagine you’re testing a service that transforms input values:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;ValueTransformer&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;strtoupper&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$value&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of writing multiple test methods, you can use a generator-based data provider:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;PHPUnit&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Framework&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Attributes&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;DataProvider&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;module&quot;&gt;PHPUnit&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Framework&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;module&quot;&gt;Attributes&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;ValueTransformerTest&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;TestCase&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;DataProvider&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;provideTransformCases&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;it_transforms_values&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$input&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$expected&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$service&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;ValueTransformer&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$service&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$input&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;        &lt;span class=&quot;variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;assertSame&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$expected&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;provideTransformCases&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;lowercase&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;hello&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;HELLO&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;mixed case&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;HeLLo&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;HELLO&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;already uppercase&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;WORLD&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;WORLD&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;24&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;25&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Making test cases more expressive&lt;/h1&gt;
&lt;p&gt;Generators really shine when your inputs are objects instead of primitives.&lt;/p&gt;
&lt;p&gt;For example, testing a query modifier:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;QueryModifier&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function-method&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;QueryBuilder&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;QueryBuilder&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your test can focus on behavior while the generator defines the variations:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;attribute&quot;&gt;DataProvider&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;provideSortingCases&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;it_applies_sorting&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$query&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;Mockery&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;mock&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;QueryBuilder&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;shouldReceive&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;orderBy&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;andReturnSelf&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$modifier&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;constructor&quot;&gt;QueryModifier&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;variable&quot;&gt;$modifier&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;function-method-call&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;variable&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;$direction&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;provideSortingCases&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;sort by name&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;asc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;sort by created_at desc&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;created_at&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;desc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;sort by nested field&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;user.email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;asc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;23&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test stays minimal, while the generator clearly documents the supported scenarios.&lt;/p&gt;
&lt;h1&gt;Composing generators&lt;/h1&gt;
&lt;p&gt;One underrated benefit is that generators can be composed. You can split cases into smaller methods and reuse them:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-php&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;provideSortingCases&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield from&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;basicFields&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield from&lt;/span&gt; &lt;span class=&quot;variable-builtin&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;nestedFields&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;basicFields&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;name asc&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;asc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;created_at desc&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;created_at&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;desc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;14&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;15&quot;&gt;&lt;span class=&quot;keyword-modifier&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;keyword-modifier&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;function&quot;&gt;nestedFields&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;type&quot;&gt;Generator&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;lbrace;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;keyword-return&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;user email&amp;#39;&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&amp;#39;user.email&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;#39;asc&amp;#39;&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;&amp;rbrace;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps large test suites maintainable without sacrificing clarity.&lt;/p&gt;
&lt;h1&gt;When to prefer generators&lt;/h1&gt;
&lt;p&gt;Generators are especially useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have many test cases&lt;/li&gt;
&lt;li&gt;Test data is constructed dynamically&lt;/li&gt;
&lt;li&gt;You want to group or reuse subsets of cases&lt;/li&gt;
&lt;li&gt;Memory usage might become a concern&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For small, static datasets, arrays are perfectly fine. But once your data providers grow, generators tend to scale much better.&lt;/p&gt;
&lt;h1&gt;Final thoughts&lt;/h1&gt;
&lt;p&gt;Using generators in PHPUnit data providers is a small change that improves readability and flexibility. Tests become easier to extend, and the intent of each case is clearer thanks to named yields.&lt;/p&gt;
&lt;p&gt;If you’re already relying on data providers, switching from arrays to generators is a low-effort improvement that pays off quickly in larger test suites.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/testing&quot;&gt;#testing&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-07T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/using-generators-for-phpunit-data-providers</id>
    <title>🐥 Using generators for PHPUnit data providers</title>
    <updated>2026-05-07T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/fixing-the-ondrej-nginx-ppa-403-error-on-laravel-forge-servers" rel="alternate"/>
    <content type="html">&lt;p&gt;If you&apos;re running Ubuntu 22.04 on a Laravel Forge-managed server, you may have recently started seeing this error during &lt;code&gt;apt update&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;Err:7 https://ppa.launchpadcontent.net/ondrej/nginx/ubuntu jammy InRelease
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  403  Forbidden
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;E: The repository &amp;#39;https://ppa.launchpadcontent.net/ondrej/nginx/ubuntu jammy InRelease&amp;#39; is no longer signed.
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What&apos;s happening?&lt;/h1&gt;
&lt;p&gt;Forge historically added the &lt;code&gt;ondrej/nginx&lt;/code&gt; PPA to its servers to get newer versions of nginx. Ondrej Surý (the maintainer) has since moved this PPA to a &lt;strong&gt;paid subscription model&lt;/strong&gt;, so unauthenticated access now returns a 403. Your server still has the old PPA entry, but it can no longer reach it.&lt;/p&gt;
&lt;p&gt;You&apos;ll notice the &lt;code&gt;ondrej/php&lt;/code&gt; PPA still works fine — that one remains freely available.&lt;/p&gt;
&lt;h1&gt;The fix&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Find the file that references the broken PPA:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;ondrej/nginx&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/apt/sources.list.d/&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Remove it (the filename will match what grep returned above):&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/apt/sources.list.d/ondrej-ubuntu-nginx-jammy.list&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/apt/sources.list.d/ondrej-ubuntu-nginx-jammy.list.distUpgrade&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;add-apt-repository --remove ppa:ondrej/nginx&lt;/code&gt; won&apos;t work here — Launchpad returns a 403 before it can resolve the PPA name.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Run &lt;code&gt;apt update&lt;/code&gt; again. If you need a newer version of nginx than Ubuntu&apos;s default (1.18.x), add the official nginx.org repository instead:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-fsSL&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;https://nginx.org/keys/nginx_signing.key&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;gpg&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--dearmor&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/usr/share/keyrings/nginx-archive-keyring.gpg&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/ubuntu jammy nginx&quot;&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;/etc/apt/sources.list.d/nginx.list&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;update&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;nginx&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Do you need a newer nginx?&lt;/h1&gt;
&lt;p&gt;For most Laravel applications, Ubuntu&apos;s default nginx is perfectly adequate. Unless you have a specific reason to run a cutting-edge nginx version, removing the PPA and sticking with the default is the simplest and most stable path.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/sysadmin&quot;&gt;#sysadmin&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-05-05T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/fixing-the-ondrej-nginx-ppa-403-error-on-laravel-forge-servers</id>
    <title>🐥 Fixing the Ondrej Nginx PPA 403 error on Laravel Forge servers</title>
    <updated>2026-05-05T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/upgrading-firebase-php-jwt-to-v7-in-a-laravel-app-transitive-dependency-trap" rel="alternate"/>
    <content type="html">&lt;p&gt;A few weeks ago we upgraded &lt;code&gt;firebase/php-jwt&lt;/code&gt; from v6 to v7 in our app. It sounds like a routine dependency bump — update the version constraint in &lt;code&gt;composer.json&lt;/code&gt;, run &lt;code&gt;composer update&lt;/code&gt;, done. But there was a catch: one of our other packages, &lt;code&gt;socialiteproviders/microsoft&lt;/code&gt;, had a hard constraint on the old version. This is the classic &lt;strong&gt;transitive dependency conflict&lt;/strong&gt;, and if you haven&apos;t run into it before, you will.&lt;/p&gt;
&lt;p&gt;Here&apos;s how we handled it and how you should too.&lt;/p&gt;
&lt;h1&gt;What&apos;s a transitive dependency conflict?&lt;/h1&gt;
&lt;p&gt;Your &lt;code&gt;composer.json&lt;/code&gt; only lists the packages you directly depend on. But each of those packages has its own &lt;code&gt;composer.json&lt;/code&gt;, listing &lt;em&gt;their&lt;/em&gt; dependencies — and those packages have their own, and so on. This tree is your full dependency graph.&lt;/p&gt;
&lt;p&gt;A transitive dependency conflict happens when you want to upgrade Package A, but Package B — which you also require — has a hard constraint that prevents it from working with the new version of A.&lt;/p&gt;
&lt;p&gt;In our case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We wanted: &lt;code&gt;firebase/php-jwt: ^7.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;socialiteproviders/microsoft&lt;/code&gt; (v4.7.1) declared: &lt;code&gt;&quot;firebase/php-jwt&quot;: &quot;^6.8&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These two constraints are mutually exclusive. Composer can&apos;t install a version of &lt;code&gt;firebase/php-jwt&lt;/code&gt; that satisfies both &lt;code&gt;^7.0&lt;/code&gt; and &lt;code&gt;^6.8&lt;/code&gt; simultaneously, so it would refuse — with a dependency resolution error.&lt;/p&gt;
&lt;h1&gt;Step 1: Understand the dependency graph before touching anything&lt;/h1&gt;
&lt;p&gt;The first move is to inspect what the problematic package actually requires:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;composer&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;socialiteproviders/microsoft&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This outputs the &lt;code&gt;requires&lt;/code&gt; section:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;requires&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;^8.0&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;ext-json&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;*&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;firebase/php-jwt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;^6.8&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;socialiteproviders/manager&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;^4.4&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There it is. Before writing a single line, you know exactly what needs to change.&lt;/p&gt;
&lt;h1&gt;Step 2: Validate with a dry run&lt;/h1&gt;
&lt;p&gt;Rather than guessing whether a combination of versions will resolve, ask Composer directly using &lt;code&gt;--dry-run&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;composer&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--dry-run&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;firebase/php-jwt:^7.0&quot;&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;socialiteproviders/microsoft:^4.9&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Composer will either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Print a clean resolution plan with the exact versions it would install, or&lt;/li&gt;
&lt;li&gt;Print an error telling you exactly which package is blocking and why&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In our case it resolved cleanly:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;Lock&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;operations:&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;installs,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;updates,&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;removals&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Upgrading&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;firebase/php-jwt&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;6.x-dev&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string-special-path&quot;&gt;v7.0.5&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;function-call&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Upgrading&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;socialiteproviders/microsoft&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;4.7.1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string-special-path&quot;&gt;4.9.1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing was written to disk. We just confirmed the upgrade path works.&lt;/p&gt;
&lt;h1&gt;Step 3: Update both packages in a single command&lt;/h1&gt;
&lt;p&gt;This is the part people get wrong. If you only update &lt;code&gt;firebase/php-jwt&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# ❌ This will fail — socialiteproviders/microsoft still requires ^6.8&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;composer&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;firebase/php-jwt&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Composer will refuse because the new &lt;code&gt;firebase/php-jwt&lt;/code&gt; v7 conflicts with the installed &lt;code&gt;socialiteproviders/microsoft&lt;/code&gt; v4.7.1. You must update both in one pass so Composer can solve the graph holistically:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# ✅ Composer solves both constraints together&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;composer&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;firebase/php-jwt&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;socialiteproviders/microsoft&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Upgrading&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;firebase/php-jwt&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;6.x-dev&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string-special-path&quot;&gt;v7.0.5&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;Extracting&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;archive&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;Upgrading&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;socialiteproviders/microsoft&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;4.7.1&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;string-special-path&quot;&gt;4.9.1&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;function-call&quot;&gt;Extracting&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;archive&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;No&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;security&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;vulnerability&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;advisories&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;found.&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Step 4: Bump the constraint in composer.json&lt;/h1&gt;
&lt;p&gt;Don&apos;t forget to update your version constraints in &lt;code&gt;composer.json&lt;/code&gt; to reflect the new minimums:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-json&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;text&quot;&gt;&quot;firebase/php-jwt&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; &quot;^7.0&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&quot;socialiteproviders/microsoft&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; &quot;^4.9&quot;&lt;/span&gt;&lt;span class=&quot;punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why &lt;code&gt;^4.9&lt;/code&gt; instead of leaving it at &lt;code&gt;^4.1&lt;/code&gt;? Because &lt;code&gt;^4.9&lt;/code&gt; documents intent: &quot;we need at least 4.9, because that&apos;s the version that supports &lt;code&gt;firebase/php-jwt&lt;/code&gt; v7.&quot; If you leave it at &lt;code&gt;^4.1&lt;/code&gt;, a future &lt;code&gt;composer update&lt;/code&gt; could theoretically resolve back to 4.7.x in some edge case and silently reintroduce the conflict.&lt;/p&gt;
&lt;h1&gt;Step 5: Check your own code for API breakage&lt;/h1&gt;
&lt;p&gt;A major version bump means potential breaking changes. Grep for every place you use the package:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&quot;Firebase\\JWT&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;app/&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--include=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;*.php&quot;&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;-l&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For &lt;code&gt;firebase/php-jwt&lt;/code&gt; specifically, &lt;code&gt;JWT::encode()&lt;/code&gt; kept the same signature in v7 — the only notable change was a stricter PHP type hint on the &lt;code&gt;$key&lt;/code&gt; parameter (&lt;code&gt;OpenSSLAsymmetricKey|OpenSSLCertificate|string&lt;/code&gt; vs the old loosely-typed &lt;code&gt;$key&lt;/code&gt;). Since we pass strings (shared secrets and RSA private keys), there was nothing to change in our &lt;code&gt;ZendeskJwtTokenBuilder&lt;/code&gt; or &lt;code&gt;DocuSignJwtTokenBuilder&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Step 6: Run the affected tests&lt;/h1&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;constant&quot;&gt;XDEBUG_MODE&lt;/span&gt;&lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;off&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;--compact&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;tests/Unit/Domains/Tokens/JwtTokenBuilderTest.php&lt;/span&gt;&lt;span class=&quot;text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;3&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;variable-parameter&quot;&gt;tests/Feature/Authentication/MicrosoftLoginTest.php&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;Tests:&lt;/span&gt;    &lt;span class=&quot;number&quot;&gt;27&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;passed&lt;/span&gt; &lt;span class=&quot;punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;number&quot;&gt;106&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;assertions&lt;/span&gt;&lt;span class=&quot;punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;Duration:&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;10.44s&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Green. Ship it.&lt;/p&gt;
&lt;h1&gt;The general playbook&lt;/h1&gt;
&lt;p&gt;Whenever you hit a transitive dependency conflict in Composer:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;composer show &lt;blocking-package&gt;&lt;/code&gt; — find the conflicting constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;composer require --dry-run &quot;a:^X&quot; &quot;b:^Y&quot;&lt;/code&gt; — validate resolution without touching files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;composer update a b&lt;/code&gt; — update all conflicting packages in one pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Tighten the version constraint in &lt;code&gt;composer.json&lt;/code&gt; to document the new minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Grep for usages, read the changelog for breaking API changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Run the affected tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The core insight: &lt;strong&gt;Composer&apos;s constraint solver needs to see the full picture&lt;/strong&gt;. If you update one package at a time when there&apos;s a conflict, you&apos;re fighting the solver instead of working with it.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/development&quot;&gt;#development&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/laravel&quot;&gt;#laravel&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/php&quot;&gt;#php&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/best-practice&quot;&gt;#best-practice&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-04-24T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/upgrading-firebase-php-jwt-to-v7-in-a-laravel-app-transitive-dependency-trap</id>
    <title>🐥 Upgrading firebase/php-jwt to v7 in a Laravel App (transitive dependency trap)</title>
    <updated>2026-04-24T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link href="https://www.yellowduck.be/posts/installing-claude-code-on-macos-with-homebrew-and-getting-the-latest-version" rel="alternate"/>
    <content type="html">&lt;p&gt;If you’re installing Claude Code on macOS using Homebrew, the official instruction currently suggests:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this works, it installs the &lt;strong&gt;latest stable version&lt;/strong&gt;, not the &lt;strong&gt;latest available version&lt;/strong&gt;. Depending on your use case, that can leave you behind on features and fixes.&lt;/p&gt;
&lt;h1&gt;Install the latest version&lt;/h1&gt;
&lt;p&gt;To install the most recent release, you should explicitly use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code@latest&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures you’re running the newest version instead of the lagging stable cask.&lt;/p&gt;
&lt;h1&gt;Fix an existing installation&lt;/h1&gt;
&lt;p&gt;If you already installed &lt;code&gt;claude-code&lt;/code&gt; using the default command, you’ll need to replace it:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;uninstall&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code@latest&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That removes the stable version and installs the latest one cleanly.&lt;/p&gt;
&lt;h1&gt;Why this matters&lt;/h1&gt;
&lt;p&gt;Homebrew distinguishes between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stable casks&lt;/strong&gt; → default installs (&lt;code&gt;claude-code&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versioned or alternative casks&lt;/strong&gt; → explicit installs (&lt;code&gt;claude-code@latest&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this case, the naming is a bit misleading because &lt;code&gt;@latest&lt;/code&gt; is not the default. This has already caused confusion in the community and is being discussed upstream.&lt;/p&gt;
&lt;h1&gt;More context&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Issue discussion: &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/42176&quot;&gt;https://github.com/anthropics/claude-code/issues/42176&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Homebrew PR: &lt;a href=&quot;https://github.com/Homebrew/homebrew-cask/pull/255221&quot;&gt;https://github.com/Homebrew/homebrew-cask/pull/255221&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Takeaway&lt;/h1&gt;
&lt;p&gt;If you want the newest Claude Code features and fixes, don’t rely on the default install. Use:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-bash&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;variable-parameter&quot;&gt;claude-code@latest&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and you’ll avoid running an outdated version without realizing it.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/tools&quot;&gt;#tools&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/announcement&quot;&gt;#announcement&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/mac&quot;&gt;#mac&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-04-02T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/installing-claude-code-on-macos-with-homebrew-and-getting-the-latest-version</id>
    <title>🐥 Installing Claude Code on macOS with Homebrew (and getting the latest version)</title>
    <updated>2026-04-02T17:00:00Z</updated>
  </entry>
</feed>