<?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 rel="alternate" href="https://www.yellowduck.be"/>
  <link rel="self" href="https://www.yellowduck.be/posts/feed"/>
  <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-08-22T17:00:00Z</updated>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/faster-leaner-mysql-backups-and-restores-with-mysqldump"/>
    <content type="html">&lt;p&gt;&lt;code&gt;mysqldump&lt;/code&gt; is a perfectly fine tool for many production workloads — until your database grows large enough that backups take too long and restores take even longer. Before you reach for a more exotic solution, there are several flags and session-level variables that can make a meaningful difference with zero infrastructure changes.&lt;/p&gt;
&lt;p&gt;This article focuses on plain MySQL, but shows where to wire up these settings when you use &lt;a href=&quot;https://github.com/spatie/laravel-backup&quot;&gt;spatie/laravel-backup&lt;/a&gt; and &lt;a href=&quot;https://github.com/stefanzweifel/laravel-backup-restore&quot;&gt;stefanzweifel/laravel-backup-restore&lt;/a&gt; in a Laravel project.&lt;/p&gt;
&lt;h1&gt;Where to configure this in Laravel&lt;/h1&gt;
&lt;p&gt;Both packages read dump and restore options from &lt;code&gt;config/database.php&lt;/code&gt;, inside the &lt;code&gt;dump&lt;/code&gt; key of your connection:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// config/database.php&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;mysql&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;// ... standard connection settings ...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;dump&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;excludeTables&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;useSingleTransaction&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;add_extra_option&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;...&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;// passed to mysqldump (backup)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;...&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;          &lt;span class=&quot;l-comment&quot;&gt;// passed to mysql client (restore)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;add_extra_option&lt;/code&gt; is forwarded to &lt;code&gt;mysqldump&lt;/code&gt; by spatie/laravel-backup.&lt;br /&gt;
&lt;code&gt;options&lt;/code&gt; is forwarded to the &lt;code&gt;mysql&lt;/code&gt; client by laravel-backup-restore.&lt;/p&gt;
&lt;p&gt;Everything below maps directly to one of those two keys.&lt;/p&gt;
&lt;h1&gt;Dump-time optimisations&lt;/h1&gt;
&lt;h2&gt;Exclude noisy, non-essential tables&lt;/h2&gt;
&lt;p&gt;Some tables accumulate millions of rows that are useful for debugging but worthless in a restore. Excluding them shrinks the dump file and cuts restore time dramatically.&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;excludeTables&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_entries&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_entries_tags&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_monitoring&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laravel Telescope is the classic example: it can easily outweigh the rest of your schema combined.&lt;/p&gt;
&lt;h2&gt;Use a single transaction&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;--single-transaction
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In spatie/laravel-backup this is:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;useSingleTransaction&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This wraps the dump in a &lt;code&gt;START TRANSACTION&lt;/code&gt; so InnoDB tables are read from a consistent snapshot without locking them. Essential for any live database.&lt;/p&gt;
&lt;h2&gt;Increase &lt;code&gt;--net-buffer-length&lt;/code&gt;&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;--net-buffer-length=16777216   # 16 MB (default is 1 MB)
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mysqldump&lt;/code&gt; groups rows into multi-row &lt;code&gt;INSERT&lt;/code&gt; statements. The default maximum per statement is 1 MB. Raising it to 16 MB reduces the total number of statements in the file, which directly cuts parse and execution time during a restore.&lt;/p&gt;
&lt;p&gt;Pair it with &lt;code&gt;--max_allowed_packet&lt;/code&gt; on the server side:&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;l-line&quot; data-line=&quot;1&quot;&gt;--max_allowed_packet=512M
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without this, large packets are silently rejected.&lt;/p&gt;
&lt;h2&gt;Skip unnecessary LOCK/UNLOCK statements&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;--skip-add-locks
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mysqldump&lt;/code&gt; normally wraps each table dump in &lt;code&gt;LOCK TABLES … WRITE&lt;/code&gt; / &lt;code&gt;UNLOCK TABLES&lt;/code&gt;. This is redundant when you are already using &lt;code&gt;--single-transaction&lt;/code&gt; and when you disable table locking at restore time (see below). Removing these statements makes the dump file smaller and the restore faster.&lt;/p&gt;
&lt;h2&gt;Suppress MySQL 8 column statistics&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;--column-statistics=0
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MySQL 8&apos;s &lt;code&gt;mysqldump&lt;/code&gt; emits &lt;code&gt;ANALYZE TABLE&lt;/code&gt; statements to update column statistics after each table is imported. On large tables these can block progress noticeably. If you run your own statistics collection after a restore, suppress them.&lt;/p&gt;
&lt;h2&gt;Compress the dump on the wire&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;--compression-algorithms=zlib
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compresses data in transit between &lt;code&gt;mysqldump&lt;/code&gt; and the MySQL server. Useful when the client and server are on separate hosts, reducing network I/O. For socket connections the benefit is minimal, but there is no downside.&lt;/p&gt;
&lt;h2&gt;Skip binary log tracking (GTID)&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;--set-gtid-purged=OFF
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you use GTID-based replication, &lt;code&gt;mysqldump&lt;/code&gt; normally writes &lt;code&gt;SET @@GLOBAL.gtid_purged&lt;/code&gt; into the dump. During a restore this can conflict with an existing &lt;code&gt;gtid_executed&lt;/code&gt; set. Passing &lt;code&gt;OFF&lt;/code&gt; omits that statement and keeps restores clean on secondary or development databases.&lt;/p&gt;
&lt;h2&gt;Avoid table-level locks&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;--lock-tables=false
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When using &lt;code&gt;--single-transaction&lt;/code&gt;, table-level locking is already unnecessary. Explicitly disabling it avoids a redundant flush.&lt;/p&gt;
&lt;h2&gt;Use &lt;code&gt;--quick&lt;/code&gt; for large tables&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;--quick
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default &lt;code&gt;mysqldump&lt;/code&gt; buffers an entire table in memory before writing. &lt;code&gt;--quick&lt;/code&gt; streams one row at a time, keeping memory usage flat regardless of table size. Always enable it for any production database.&lt;/p&gt;
&lt;h1&gt;Restore-time optimisations&lt;/h1&gt;
&lt;p&gt;These are passed to the &lt;code&gt;mysql&lt;/code&gt; client via an &lt;code&gt;--init-command&lt;/code&gt;, which MySQL executes before the dump is processed:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;sql_log_bin&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;foreign_key_checks&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;unique_checks&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;autocommit&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;config/database.php&lt;/code&gt;:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;--init-command=&quot;SET sql_log_bin=0; SET foreign_key_checks=0; SET unique_checks=0; SET autocommit=0;&quot;&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;SET sql_log_bin=0&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Disables binary logging for the session. There is no point logging every INSERT from a dump file into the binlog — it inflates the binlog and slows the restore. Safe when you control the import and are not relying on the binlog to propagate changes to replicas. This also reduces the amount of disk space needed for the restore.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;SET foreign_key_checks=0&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;MySQL enforces referential integrity row-by-row during inserts. In a full dump, the parent and child rows are all present — you just may not have inserted the parent yet when a child arrives. Disabling this check lets MySQL trust the dump and skip the per-row lookups, which is one of the most impactful restore optimisations available.&lt;/p&gt;
&lt;p&gt;Re-enable it (or restart the session) after the restore to verify the restored data is consistent.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;SET unique_checks=0&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Similar rationale: MySQL normally verifies unique constraints on every insert. A correctly generated dump has no duplicates, so this check is redundant overhead. Disabling it reduces index update cost during the restore.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;SET autocommit=0&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Turns off the implicit &lt;code&gt;COMMIT&lt;/code&gt; after every statement. Combined with the large &lt;code&gt;INSERT&lt;/code&gt; batches produced by &lt;code&gt;--net-buffer-length&lt;/code&gt;, this means the storage engine can batch many rows into a single transaction, dramatically reducing fsync pressure.&lt;/p&gt;
&lt;h1&gt;Putting it together&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// config/database.php&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;dump&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;excludeTables&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_entries&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_entries_tags&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;telescope_monitoring&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;useSingleTransaction&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;add_extra_option&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;--set-gtid-purged=OFF --lock-tables=false --quick&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39; --max_allowed_packet=512M --net-buffer-length=16777216&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39; --compression-algorithms=zlib&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39; --skip-add-locks --column-statistics=0&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;--init-command=&quot;SET sql_log_bin=0; SET foreign_key_checks=0; SET unique_checks=0; SET autocommit=0;&quot;&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These settings are safe for the vast majority of production MySQL 8 setups. The restore-side flags assume you trust the dump and will verify data integrity via application-level health checks afterwards — which is standard practice regardless of how you import.&lt;/p&gt;
&lt;p&gt;The backup side produces a smaller file, faster. The restore side skips redundant constraint checking and batches commits. Together they can reduce both phases by a significant margin without changing anything about your infrastructure or your backup tooling.&lt;/p&gt;
&lt;h1&gt;More info&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://spatie.be/docs/laravel-backup/v10/introduction&quot;&gt;Spatie Backup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stefanzweifel.dev/posts/2023/06/15/introducing-laravel-backup-restore/&quot;&gt;Introducing laravel-backup-restore
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/9.7/en/mysqldump.html&quot;&gt;mysqldump — A Database Backup Program&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/9.7/en/mysql.html&quot;&gt;mysql — The MySQL Command-Line Client&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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/mysql&quot;&gt;#mysql&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/devops&quot;&gt;#devops&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-08-22T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/faster-leaner-mysql-backups-and-restores-with-mysqldump</id>
    <title>🐥 Faster, leaner MySQL backups and restores with mysqldump</title>
    <updated>2026-08-22T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/speeding-up-our-php-test-suite-with-a-github-actions-matrix-strategy"/>
    <content type="html">&lt;p&gt;Our test suite had become a bottleneck. Every push to &lt;code&gt;develop&lt;/code&gt; or &lt;code&gt;main&lt;/code&gt; triggered three separate CI jobs — unit tests, feature tests, and integration tests — each running sequentially after the previous one finished and each downloading the same apt packages from scratch. The result: slow feedback loops and a lot of redundant work.&lt;/p&gt;
&lt;p&gt;This post walks through the changes we made to fix that.&lt;/p&gt;
&lt;h1&gt;The Problem: Three Jobs, Lots of Duplication&lt;/h1&gt;
&lt;p&gt;Before this change, the CI pipeline defined three independent jobs:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;phpunit-unit&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;PHP Unit Tests&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Install packages&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;|&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;update&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-y&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;package-a&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;package-b&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;package-c&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Execute tests&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--parallel&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;tests/Unit&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;phpunit-feature&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;PHP Feature Tests&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;# same apt install block...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Execute tests&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--parallel&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;tests/Feature&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;phpunit-integration&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;PHP Integration Tests&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;# same apt install block again...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Execute tests&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--parallel&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;tests/Integration&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each job was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Checking out the repo&lt;/strong&gt; independently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Setting up PHP&lt;/strong&gt; independently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Installing Composer dependencies&lt;/strong&gt; independently (even with caching, cache restores take time)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Running &lt;code&gt;apt-get install&lt;/code&gt;&lt;/strong&gt; with the same packages — every single time, with no caching&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Step 1: Collapsing Three Jobs into a Matrix&lt;/h1&gt;
&lt;p&gt;The first change replaced the three separate job definitions with a single &lt;code&gt;phpunit&lt;/code&gt; job that uses GitHub Actions&apos; &lt;code&gt;matrix&lt;/code&gt; strategy:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;phpunit&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;PHP Tests (${{ matrix.suite }})&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;ubuntu-latest&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;strategy&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;fail-fast&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;false&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;matrix&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;unit-a&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;unit-b&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;feature-a&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;feature-b&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Integration&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All suites run in parallel, with &lt;code&gt;fail-fast: false&lt;/code&gt; so a failure in one suite doesn&apos;t cancel the others mid-run.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Execute tests&lt;/code&gt; step became a single parameterised command:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Execute tests&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;php&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;artisan&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--parallel&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--testsuite=&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;${{ matrix.suite &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt; --coverage-clover test-coverage/coverage-${{ matrix.suite &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;.xml&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why More Suites Instead of Fewer?&lt;/h2&gt;
&lt;p&gt;The original split was coarse: all unit tests ran together, and all feature tests ran together. One large bucket would hold everything up even when most of the tests in it finished quickly.&lt;/p&gt;
&lt;p&gt;By naming suites explicitly in &lt;code&gt;phpunit.xml&lt;/code&gt;, we got finer-grained parallelism:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-xml&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;unit-a&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;.php&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;./tests/Unit/GroupA&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;unit-b&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;.php&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;./tests/Unit/GroupB&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;.php&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;./tests/Unit/GroupC&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;feature-a&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;.php&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;./tests/Feature/GroupA&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;feature-b&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;.php&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;./tests/Feature/GroupB&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;.php&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;./tests/Feature/GroupC&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;testsuite&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps each matrix leg roughly balanced in terms of test count and makes the overall wall-clock time shorter than when one slow bucket holds everything up.&lt;/p&gt;
&lt;h1&gt;Step 2: Caching apt Packages&lt;/h1&gt;
&lt;p&gt;The most wasteful part of the old setup was reinstalling the same system packages on every runner, for every job, from scratch. &lt;code&gt;apt-get install&lt;/code&gt; isn&apos;t slow in absolute terms, but multiplied across five parallel jobs and every CI run, it adds up.&lt;/p&gt;
&lt;p&gt;We replaced the raw &lt;code&gt;apt install&lt;/code&gt; block with &lt;a href=&quot;https://github.com/awalsh128/cache-apt-pkgs-action&quot;&gt;&lt;code&gt;awalsh128/cache-apt-pkgs-action&lt;/code&gt;&lt;/a&gt;:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Install packages&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;awalsh128/cache-apt-pkgs-action@latest&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;package-a package-b package-c&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;${{ hashFiles(&amp;#39;.apt-packages&amp;#39;) }}&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;version&lt;/code&gt; key is tied to a &lt;code&gt;.apt-packages&lt;/code&gt; lockfile committed to the repo. The cache is invalidated only when that file changes — not on every push — so the common case is a fast cache hit rather than a full install.&lt;/p&gt;
&lt;p&gt;Any post-install configuration that previously lived inside the install block becomes its own named step. The action calls it only on a cache miss, so it doesn&apos;t run unnecessarily on cache hits:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Post-install configuration&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;# your configuration command here&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;The Downstream Effect: Coverage Merging&lt;/h1&gt;
&lt;p&gt;Any job that previously depended on the three separate job names now depends on the single &lt;code&gt;phpunit&lt;/code&gt; job:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;some-downstream-job&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;&quot;phpunit&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;   &lt;span class=&quot;l-comment&quot;&gt;# was: phpunit-unit, phpunit-feature, phpunit-integration&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-string&quot;&gt;...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the coverage merge step includes one file per suite:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;./clover-merge&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;coverage.xml&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;unit-a/coverage-unit-a.xml&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;unit-b/coverage-unit-b.xml&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;feature-a/coverage-feature-a.xml&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;feature-b/coverage-feature-b.xml&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;integration/coverage-integration.xml&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What This Changes&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3 jobs, one per test type&lt;/td&gt;
&lt;td&gt;5 jobs, all parallel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apt install&lt;/code&gt; on every run, every job&lt;/td&gt;
&lt;td&gt;Cached apt packages, invalidated only on lockfile change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coarse path-based test selection&lt;/td&gt;
&lt;td&gt;Named testsuites in &lt;code&gt;phpunit.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~3× duplicated job boilerplate&lt;/td&gt;
&lt;td&gt;Single parameterised job definition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The wall-clock time for the PHP test stage drops because the legs run concurrently and the apt install step is usually a cache hit. Maintenance is easier too — adding a new suite is a two-line change: one entry in &lt;code&gt;phpunit.xml&lt;/code&gt; and one entry in the matrix &lt;code&gt;include&lt;/code&gt; list.&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/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/github&quot;&gt;#github&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-08-20T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/speeding-up-our-php-test-suite-with-a-github-actions-matrix-strategy</id>
    <title>🐥 Speeding up our PHP test suite with a GitHub Actions matrix strategy</title>
    <updated>2026-08-20T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/advanced-debugging-in-elixir-with-io-inspect"/>
    <content type="html">&lt;p&gt;When writing Elixir, most developers quickly get familiar with &lt;a href=&quot;https://hexdocs.pm/elixir/IO.html#inspect/2&quot;&gt;&lt;code&gt;IO.inspect/2&lt;/code&gt;&lt;/a&gt; as a quick way to see what&apos;s happening inside their code. But what many overlook is that &lt;code&gt;IO.inspect&lt;/code&gt; is far more powerful than just &quot;print this variable to the console.&quot;&lt;/p&gt;
&lt;p&gt;In fact, with the right options and placement, &lt;code&gt;IO.inspect&lt;/code&gt; can become a precise, highly targeted debugging tool, one that doesn&apos;t interrupt your program flow and works seamlessly with Elixir&apos;s functional pipelines.&lt;/p&gt;
&lt;p&gt;This post will walk through both the fundamentals and advanced patterns for using &lt;code&gt;IO.inspect&lt;/code&gt; effectively. By the end, you&apos;ll know how to control output formatting, label your prints for clarity, debug concurrent processes, and even integrate conditional or file-based inspection.&lt;/p&gt;
&lt;h1&gt;The Basics of &lt;code&gt;IO.inspect&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://hexdocs.pm/elixir/IO.html#inspect/2&quot;&gt;&lt;code&gt;IO.inspect&lt;/code&gt;&lt;/a&gt; is defined 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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;\\&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;item&lt;/strong&gt;: any Elixir value you want to inspect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;opts&lt;/strong&gt;: keyword list of options for controlling how the term is printed as defined in &lt;a href=&quot;https://hexdocs.pm/elixir/Inspect.Opts.html&quot;&gt;&lt;code&gt;Inspect.Opts&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By default, it writes the inspected value to the standard output (&lt;code&gt;:stdio&lt;/code&gt;) and then returns the term unchanged.&lt;/p&gt;
&lt;p&gt;That last part is key. Because it returns the term, you can drop it anywhere in a function or pipeline without breaking the flow.&lt;/p&gt;
&lt;p&gt;Example:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Demo&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;age: &lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prints:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;age: &lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Caveat: When &lt;code&gt;IO.inspect&lt;/code&gt; Is the Last Call in a Function&lt;/h1&gt;
&lt;p&gt;Because &lt;code&gt;IO.inspect&lt;/code&gt; returns its argument, it also means that if it&apos;s the &lt;strong&gt;last expression in your function&lt;/strong&gt;, that inspected value becomes the function&apos;s return value.&lt;/p&gt;
&lt;p&gt;That&apos;s fine if you &lt;em&gt;intend&lt;/em&gt; to return it, but it can lead to subtle bugs if you were only printing it for debugging and expected a different return.&lt;/p&gt;
&lt;p&gt;Take this example:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;fetch_user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-module&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Fetched user&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the function returns the user struct as normal, which is fine.&lt;/p&gt;
&lt;p&gt;Now, consider:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;log_and_return&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Done!&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This returns &lt;code&gt;&quot;Done!&quot;&lt;/code&gt; instead of, say, &lt;code&gt;:ok&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you want to print something &lt;strong&gt;but return a different value&lt;/strong&gt;, make the return explicit:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;log_and_return&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Done!&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or if you&apos;re in a pipeline:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;value&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Debug&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;do_something_else&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; if &lt;code&gt;IO.inspect&lt;/code&gt; is the last expression in a function, be explicit about what you want to return.&lt;/p&gt;
&lt;h1&gt;Strategic Placement With the Pipe Operator&lt;/h1&gt;
&lt;p&gt;Because &lt;code&gt;IO.inspect&lt;/code&gt; returns its argument, it fits perfectly in the middle of a pipeline:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;users&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Before filtering&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;active&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;After filtering&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lets you peek into the data flow at exactly the point you want, without rewriting your code into intermediate variables.&lt;/p&gt;
&lt;p&gt;A neat trick is to place multiple &lt;code&gt;IO.inspect&lt;/code&gt; calls at different stages, each with a distinct label, so you can see how the data changes step by step.&lt;/p&gt;
&lt;h1&gt;Using Labels for Clarity&lt;/h1&gt;
&lt;p&gt;Without labels, multiple inspection outputs can be hard to tell apart. The &lt;code&gt;label&lt;/code&gt; option solves 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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;After filtering&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prints:&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;l-line&quot; data-line=&quot;1&quot;&gt;After filtering: [%{name: &quot;Alice&quot;}, %{name: &quot;Bob&quot;}]
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When debugging a pipeline with several inspect points, labels make the output self-describing. This is especially useful when you&apos;re debugging multiple similar data structures in the same run.&lt;/p&gt;
&lt;h1&gt;Pretty Printing and Formatting Output&lt;/h1&gt;
&lt;p&gt;Sometimes, especially with large maps or deeply nested lists, the default single-line output is hard to read. That&apos;s where formatting options defined in &lt;a href=&quot;https://hexdocs.pm/elixir/Inspect.Opts.html&quot;&gt;&lt;code&gt;Inspect.Opts&lt;/code&gt;&lt;/a&gt; come in.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:pretty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;if set to &lt;code&gt;true&lt;/code&gt; enables pretty printing.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;limits the number of items that are inspected for tuples, bitstrings, maps, lists and any other collection of items, with the exception of printable strings and printable charlists which use the &lt;code&gt;:printable_limit&lt;/code&gt; option. If you don&apos;t want to limit the number of items to a particular number, use &lt;code&gt;:infinity&lt;/code&gt;. It accepts a positive integer or &lt;code&gt;:infinity&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:printable_limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;limits the number of characters that are inspected on printable strings and printable charlists. You can use &lt;a href=&quot;https://hexdocs.pm/elixir/String.html#printable?/1&quot;&gt;&lt;code&gt;String.printable?/1&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://hexdocs.pm/elixir/List.html#ascii_printable?/1&quot;&gt;&lt;code&gt;List.ascii_printable?/1&lt;/code&gt;&lt;/a&gt; to check if a given string or charlist is printable. If you don&apos;t want to limit the number of characters to a particular number, use &lt;code&gt;:infinity&lt;/code&gt;. It accepts a positive integer or &lt;code&gt;:infinity&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4096&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:width&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number of characters per line used when pretty is &lt;code&gt;true&lt;/code&gt; or when printing to IO devices. Set to &lt;code&gt;0&lt;/code&gt; to force each item to be printed on its own line. If you don&apos;t want to limit the number of items to a particular number, use &lt;code&gt;:infinity&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;80&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:charlists&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when &lt;code&gt;:as_charlists&lt;/code&gt; all lists will be printed as charlists, non-printable elements will be escaped. When &lt;code&gt;:as_lists&lt;/code&gt; all lists will be printed as lists. When the default &lt;code&gt;:infer&lt;/code&gt;, the list will be printed as a charlist if it is printable, otherwise as list. See &lt;a href=&quot;https://hexdocs.pm/elixir/List.html#ascii_printable?/1&quot;&gt;&lt;code&gt;List.ascii_printable?/1&lt;/code&gt;&lt;/a&gt; to learn when a charlist is printable.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:infer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Example:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;value: &lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;duplicate&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;x&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;pretty: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;limit: &lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prints:&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;l-line&quot; data-line=&quot;1&quot;&gt;[
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  %{id: 1, value: &quot;x&quot;},
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  %{id: 2, value: &quot;xx&quot;},
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  %{id: 3, value: &quot;xxx&quot;},
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  %{id: 4, ...},
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  %{...},
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  ...
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;]
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we can see the first five entries, and the rest are summarized with &lt;code&gt;...&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Coloring and Styling Output&lt;/h1&gt;
&lt;p&gt;Elixir&apos;s inspect options support syntax coloring, very handy when your terminal is full of logs.&lt;/p&gt;
&lt;p&gt;Example:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;syntax_colors: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;atom: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:blue&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;string: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:green&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;number: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:red&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;pretty: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes atoms blue, strings green, and numbers red in your console output.&lt;/p&gt;
&lt;p&gt;Colors can be any &lt;a href=&quot;https://hexdocs.pm/elixir/IO.ANSI.html#t:ansidata/0&quot;&gt;&lt;code&gt;IO.ANSI.ansidata/0&lt;/code&gt;&lt;/a&gt; as accepted by &lt;a href=&quot;https://hexdocs.pm/elixir/IO.ANSI.html#format/1&quot;&gt;&lt;code&gt;IO.ANSI.format/1&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you want to use the default colors (like what is used in &lt;code&gt;IEx&lt;/code&gt;), you can use &lt;a href=&quot;https://hexdocs.pm/elixir/IO.ANSI.html#syntax_colors/0&quot;&gt;&lt;code&gt;IO.ANSI.syntax_colors/0&lt;/code&gt;&lt;/a&gt;:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;syntax_colors: &lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;IO.ANSI&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;syntax_colors&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;pretty: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The colors are only visible if your terminal supports ANSI colors.&lt;/p&gt;
&lt;h1&gt;Conditional Inspection&lt;/h1&gt;
&lt;p&gt;Sometimes you want to inspect only if a certain condition is true, for example, when a debug flag is enabled or when a value crosses a threshold.&lt;/p&gt;
&lt;p&gt;Here&apos;s an example with a debug flag:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;term&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;get_env&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:my_app&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:debug&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;term&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Debug&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;term&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can use this function 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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;users&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;active&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can toggle the output by setting &lt;code&gt;config :my_app, :debug, true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Capturing Inspect Output Instead of Printing&lt;/h1&gt;
&lt;p&gt;If you want to get the inspected form of a value without printing it, use &lt;a href=&quot;https://hexdocs.pm/elixir/Inspect.html#inspect/2&quot;&gt;&lt;code&gt;inspect/2&lt;/code&gt;&lt;/a&gt; (without the &lt;code&gt;IO.&lt;/code&gt; prefix):&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;string_representation&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;pretty: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is useful if you want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write the debug output to a file&lt;/li&gt;
&lt;li&gt;Send it to a logging service&lt;/li&gt;
&lt;li&gt;Include it in an exception message&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-elixir&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;write!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;debug.log&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;pretty: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also redirect &lt;code&gt;IO.inspect&lt;/code&gt; output to another device:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Debug&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;device: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:stderr&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Debugging Concurrency and Async Code&lt;/h1&gt;
&lt;p&gt;When you use &lt;code&gt;IO.inspect&lt;/code&gt; in asynchronous code, the output may arrive out of order, making it hard to follow. Adding identifiers or timestamps can help.&lt;/p&gt;
&lt;p&gt;Example:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;tasks&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;      &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;PID &lt;/span&gt;&lt;span class=&quot;l-string-special&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-string-special&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;l-type&quot;&gt;:timer&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prints:&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;l-line&quot; data-line=&quot;1&quot;&gt;PID 3: #PID&lt;0.112.0&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;PID 2: #PID&lt;0.111.0&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;PID 1: #PID&lt;0.110.0&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Including the PID or a unique request ID in your label helps you trace which output belongs to which process.&lt;/p&gt;
&lt;h1&gt;Advanced Trick: Inspecting and Pattern Matching in One Go&lt;/h1&gt;
&lt;p&gt;You can combine pattern matching with inspection to see exactly what&apos;s being matched:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;User before insert&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This inspects the &lt;code&gt;user&lt;/code&gt; variable before pattern matching extracts the &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can also place &lt;code&gt;IO.inspect&lt;/code&gt; inside a guard to conditionally print:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;role: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:admin&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;u&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Admin user&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this last example, &quot;Admin user&quot; will only be printed if the user has the role &lt;code&gt;:admin&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Using &lt;code&gt;dbg/2&lt;/code&gt; for Richer Inspection&lt;/h1&gt;
&lt;p&gt;Since Elixir &lt;strong&gt;1.14&lt;/strong&gt;, we have &lt;a href=&quot;https://hexdocs.pm/elixir/Kernel.html#dbg/2&quot;&gt;&lt;code&gt;dbg/2&lt;/code&gt;&lt;/a&gt;, a built-in debugging helper that works like &lt;code&gt;IO.inspect&lt;/code&gt; but &lt;strong&gt;also shows the code expression&lt;/strong&gt; that produced the value.&lt;/p&gt;
&lt;p&gt;You can use it 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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;active&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;dbg&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which prints:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;#=&gt; [%{active: false, name: &quot;Alice&quot;}, %{active: true, name: &quot;Bob&quot;}]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;active&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;#=&gt; [%{active: true, name: &quot;Bob&quot;}]&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it much easier to understand &lt;em&gt;where&lt;/em&gt; in your code the inspected value is coming from, especially when you&apos;re inspecting multiple similar-looking values.&lt;/p&gt;
&lt;p&gt;You can also pass options similar to &lt;code&gt;IO.inspect&lt;/code&gt;:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;dbg&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Active users&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;pretty: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are the key differences with &lt;code&gt;IO.inspect&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shows the code expression&lt;/strong&gt; automatically.&lt;/li&gt;
&lt;li&gt;Output format is slightly more verbose.&lt;/li&gt;
&lt;li&gt;Same return behavior, returns the inspected value, so you can keep it in a pipeline.&lt;/li&gt;
&lt;li&gt;Best for &lt;strong&gt;interactive debugging&lt;/strong&gt; during development.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;When &lt;code&gt;IO.inspect&lt;/code&gt; Is Not Enough&lt;/h1&gt;
&lt;p&gt;While &lt;code&gt;IO.inspect&lt;/code&gt; is a fantastic quick-and-dirty tool, there are times when you need more powerful debugging:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://hexdocs.pm/iex/IEx.html#pry/0&quot;&gt;&lt;code&gt;IEx.pry&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;: drops you into an interactive REPL inside the running process.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.erlang.org/doc/apps/observer/observer.html#start/0&quot;&gt;&lt;code&gt;:observer.start/0&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;: Erlang&apos;s GUI for monitoring processes, memory, and more.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trick is to know when to reach for &lt;code&gt;IO.inspect&lt;/code&gt; and when to switch to one of these other tools.&lt;/p&gt;
&lt;h1&gt;Setting Default Options for &lt;code&gt;IO.inspect&lt;/code&gt; in IEx&lt;/h1&gt;
&lt;p&gt;When using &lt;code&gt;IEx&lt;/code&gt;, you can configure the default options for &lt;code&gt;IO.inspect&lt;/code&gt;. You can do this using the &lt;a href=&quot;https://hexdocs.pm/iex/IEx.html#configure/1&quot;&gt;&lt;code&gt;IEx.configure/1&lt;/code&gt;&lt;/a&gt; function:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-module&quot;&gt;IEx&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;configure&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-string-special-symbol&quot;&gt;colors: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;syntax_colors: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;number: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:light_yellow&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;atom: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:light_cyan&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;string: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:light_black&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;boolean: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:red&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;nil: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:magenta&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:bright&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;ls_directory: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:cyan&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;ls_device: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:yellow&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;doc_code: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:green&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;doc_inline_code: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:magenta&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;doc_headings: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:cyan&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:underline&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;doc_title: &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:cyan&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:bright&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:underline&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can easily put this in your &lt;a href=&quot;https://hexdocs.pm/iex/IEx.html#module-configuring-the-shell&quot;&gt;&lt;code&gt;.iex.exs&lt;/code&gt;&lt;/a&gt; file so that it&apos;s applied automatically every time you open the shell.&lt;/p&gt;
&lt;h1&gt;Creating a Reusable Inspect Helper&lt;/h1&gt;
&lt;p&gt;Here&apos;s a neat way to wrap &lt;code&gt;IO.inspect/2&lt;/code&gt; into your own helper module with preferred defaults. This way, you can keep consistent inspection output without repeating options everywhere:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Debug&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-comment&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;moduledoc&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;&quot;&quot;&quot;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  Convenience wrapper around &lt;/span&gt;&lt;span class=&quot;l-markup-raw&quot;&gt;`IO.inspect/2`&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt; with sensible defaults.
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &quot;&quot;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;default_opts &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;DEBUG&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;pretty: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;limit: &lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:infinity&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;width: &lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;120&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-string-special-symbol&quot;&gt;syntax_colors: &lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;IO.ANSI&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;syntax_colors&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;l-comment&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;&quot;&quot;&quot;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;  Inspects a value with default debug options.
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;  This is pipe-friendly: it returns the given value unchanged
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;  after inspecting it.
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-markup-heading-2&quot;&gt;  ## Examples
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;&lt;span class=&quot;l-markup-raw-block&quot;&gt;      iex&gt; [1, 2, 3]
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;&lt;/span&gt;    &lt;span class=&quot;l-markup-raw-block&quot;&gt;  ...&gt; |&gt; Enum.map(&amp;(&amp;1 * 2))
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;&lt;/span&gt;    &lt;span class=&quot;l-markup-raw-block&quot;&gt;  ...&gt; |&gt; MyApp.Debug.inspect()
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;&lt;/span&gt;    &lt;span class=&quot;l-markup-raw-block&quot;&gt;  [2, 4, 6]
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;term&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;\\&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;term&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Keyword&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;default_opts&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can use it 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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;users&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;active?&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Debug&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Active users&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Debug&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;User names&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You get pretty printing (&lt;code&gt;pretty: true&lt;/code&gt;) by default.&lt;/li&gt;
&lt;li&gt;Lists and maps are not truncated (&lt;code&gt;limit: :infinity&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A label is always shown (&lt;code&gt;DEBUG&lt;/code&gt; unless overridden).&lt;/li&gt;
&lt;li&gt;You can still override any option per call.&lt;/li&gt;
&lt;li&gt;It works fine when it&apos;s in a pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Handy Visual Studio Code snippets&lt;/h1&gt;
&lt;p&gt;To make using &lt;code&gt;IO.inspect&lt;/code&gt; faster and more consistent, you can configure editor snippets in Visual Studio Code so you don&apos;t have to type repetitive boilerplate each time.&lt;/p&gt;
&lt;p&gt;In Visual Studio Code, open:&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;l-line&quot; data-line=&quot;1&quot;&gt;Code -&gt; Settings… -&gt; Configure Snippets -&gt; Elixir
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and add the following snippet definitions:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;&quot;inpsect&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;prefix&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;lin&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;IO.inspect($1, label: &lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;, pretty: true)&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;IO.inspect with label.&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;&quot;inpsectSelectedText&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;prefix&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;sin&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;IO.inspect($TM_SELECTED_TEXT, label: &lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;${TM_SELECTED_TEXT/(.*)/${1:/upcase}/}$0&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;, pretty: true)&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;IO.inspect with selected text as label.&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;&quot;pipeInspect&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;prefix&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;pin&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;|&gt; IO.inspect(label: &lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;, pretty: true)&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;IO.inspect with pipe.&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;&quot;pipeInspectWithFileReference&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;prefix&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;pinf&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;|&gt; IO.inspect(label: &lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;$TM_FILEPATH:$TM_LINE_NUMBER$0&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;, pretty: true)&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;IO.inspect with pipe and file reference.&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;&quot;inspectFromClipboard&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;prefix&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;cin&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;IO.inspect($CLIPBOARD, label: &lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;${CLIPBOARD/(.*)/${1:/upcase}/}$0&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;, pretty: true)&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;IO.inspect clipboard content.&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With these in place, you&apos;ll have short prefixes to insert commonly used &lt;code&gt;IO.inspect/2&lt;/code&gt; patterns:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IO.inspect&lt;/code&gt; with label&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IO.inspect&lt;/code&gt; using selected text as label&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IO.inspect&lt;/code&gt; in a pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pinf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IO.inspect&lt;/code&gt; in a pipeline with file and line reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IO.inspect&lt;/code&gt; clipboard content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This way, you can quickly drop in debug output with consistent formatting, colors, and labels, without breaking your flow.&lt;/p&gt;
&lt;p&gt;Suppose you are transforming a list of user maps and want to check intermediate results inside a pipeline:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;users&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;active&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;After filter&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;pretty: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the snippet defined above, you don&apos;t have to type all that. Just type &lt;code&gt;pin&lt;/code&gt;, hit &lt;strong&gt;Tab&lt;/strong&gt;, and you get:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;inspect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;pretty: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can immediately type your label (e.g. &lt;code&gt;&quot;After filter&quot;&lt;/code&gt;) and continue coding. This keeps your debugging consistent, colorful, and fast, without breaking your flow.&lt;/p&gt;
&lt;h1&gt;Best practices&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use labels liberally&lt;/strong&gt;, unlabeled output is harder to parse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limit output&lt;/strong&gt; for large collections, use &lt;code&gt;limit&lt;/code&gt; and &lt;code&gt;pretty&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid leaving it in production&lt;/strong&gt; unless intentional.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrap it&lt;/strong&gt; in helper functions when you want conditional control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tag concurrent output&lt;/strong&gt; with PIDs, timestamps, or request IDs.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hexdocs.pm/credo/overview.html&quot;&gt;&lt;strong&gt;Credo&lt;/strong&gt;&lt;/a&gt; can be used to detect unintentional calls to &lt;code&gt;IO.inspect&lt;/code&gt; in a CI/CD pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;IO.inspect&lt;/code&gt; may look like a humble debugging tool, but in Elixir it&apos;s a powerful way to see what&apos;s going on without breaking your code&apos;s flow. By combining it with labels, formatting, conditional output, and process context, you can get precise insights into your program&apos;s behavior, all without leaving your editor.&lt;/p&gt;&lt;p&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;/p&gt;</content>
    <published>2026-08-17T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/advanced-debugging-in-elixir-with-io-inspect</id>
    <title>🐥 Advanced debugging in Elixir with IO.inspect</title>
    <updated>2026-08-17T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/my-thoughts-on-the-future-of-go-in-the-ai-era"/>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;What if the rise of AI makes programming languages like Go more valuable? Alex Pliutau argues that while languages like TypeScript and Rust dominate the discussion, Go offers unique advantages in the AI landscape. With its massive standard library, fast compilation, and commitment to stability, Go could thrive even as coding agents proliferate. This article dissects Go&apos;s critical features—like explicit error handling and efficient concurrency—that align well with AI-generated code needs. Pliutau suggests that Go&apos;s simplicity and practicality may become invaluable in an era where comprehensible and maintainable code is essential.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://packagemain.tech/p/my-thoughts-on-the-future-of-go-in-ai-era&quot;&gt;Continue reading on &lt;strong&gt;packagemain.tech&lt;/strong&gt;&lt;/a&gt;&lt;p&gt;&lt;/p&gt;</content>
    <published>2026-08-15T13:00:00Z</published>
    <id>https://www.yellowduck.be/posts/my-thoughts-on-the-future-of-go-in-the-ai-era</id>
    <title>🔗 My thoughts on the future of Go in the AI era</title>
    <updated>2026-08-15T13:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/hosting-your-own-apt-repository-for-a-go-cli-tool"/>
    <content type="html">&lt;p&gt;Distributing a command-line tool to Linux users is straightforward until you want to make it feel native. Tarballs work, but APT packages are what seasoned Linux users expect: a single &lt;code&gt;apt install&lt;/code&gt;, automatic updates via &lt;code&gt;apt upgrade&lt;/code&gt;, and clean removal with &lt;code&gt;apt remove&lt;/code&gt;. This post walks through how we set up a self-hosted APT repository for a Go CLI tool using only a Makefile, a small shell script, and a clever Go utility called &lt;code&gt;aptblob&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;The goal&lt;/h1&gt;
&lt;p&gt;We want users to be able to run:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-fsSL&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;https://packages.example.com/pubkey.gpg&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;apt-key&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;deb https://packages.example.com stable main&quot;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;/etc/apt/sources.list.d/mycli.list&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;update&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;mycli&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make that work, we need to produce properly structured &lt;code&gt;.deb&lt;/code&gt; packages for each architecture, sign them with a GPG key, and publish an APT repository index that &lt;code&gt;apt&lt;/code&gt; can parse.&lt;/p&gt;
&lt;h1&gt;Step 1: cross-compiling and packaging&lt;/h1&gt;
&lt;p&gt;Go makes cross-compilation trivial. The &lt;code&gt;build-deb&lt;/code&gt; make target compiles the binary twice — once for &lt;code&gt;arm64&lt;/code&gt; and once for &lt;code&gt;amd64&lt;/code&gt; — and wraps each in a proper Debian package.&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-make&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;## build-deb: build the debian package&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;.PHONY&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;build-deb&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-function&quot;&gt;build-deb&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    @rm -f ./&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;APPNAME&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;-*
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; build-binary,linux,arm64,arm64&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; build-deb,arm64&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; build-binary,linux,amd64,x86_64&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; build-deb,amd64&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;build-binary&lt;/code&gt; helper looks like this:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-make&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;define&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;build-binary&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    @GOOS=$(1) GOARCH=$(2) go build -o $(APPNAME)
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;endef&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;build-deb&lt;/code&gt; helper function does all the Debian packaging work. It creates the directory structure that &lt;code&gt;dpkg-deb&lt;/code&gt; expects, writes a minimal &lt;code&gt;DEBIAN/control&lt;/code&gt; file, places the binary into &lt;code&gt;usr/bin/&lt;/code&gt;, and calls &lt;code&gt;dpkg-deb --build&lt;/code&gt; to produce the &lt;code&gt;.deb&lt;/code&gt; archive:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-make&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;define&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;build-deb&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    @mkdir -p $(APPNAME)_$(VERSION)-1_$(1)/DEBIAN
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    @mkdir -p $(APPNAME)_$(VERSION)-1_$(1)/usr/bin
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    @mv $(APPNAME) $(APPNAME)_$(VERSION)-1_$(1)/usr/bin/$(APPNAME)
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    @echo &quot;Package: mycli&quot;              &gt; $(APPNAME)_$(VERSION)-1_$(1)/DEBIAN/control
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    @echo &quot;Version: $(VERSION)&quot;        &gt;&gt; $(APPNAME)_$(VERSION)-1_$(1)/DEBIAN/control
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    @echo &quot;Maintainer: Team &lt;hi@example.com&gt;&quot; &gt;&gt; $(APPNAME)_$(VERSION)-1_$(1)/DEBIAN/control
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    @echo &quot;Architecture: $(1)&quot;         &gt;&gt; $(APPNAME)_$(VERSION)-1_$(1)/DEBIAN/control
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    @echo &quot;Description: My CLI tool&quot;   &gt;&gt; $(APPNAME)_$(VERSION)-1_$(1)/DEBIAN/control
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    @dpkg-deb --build -Zgzip $(APPNAME)_$(VERSION)-1_$(1)
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    @rm -rf $(APPNAME)_$(VERSION)-1_$(1)
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;endef&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-Zgzip&lt;/code&gt; flag tells &lt;code&gt;dpkg-deb&lt;/code&gt; to use gzip compression, which produces slightly larger packages than xz but is more universally compatible.&lt;/p&gt;
&lt;p&gt;The version is injected automatically from the latest git tag:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-make&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;VERSION&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;shell&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--tags&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--abbrev=0&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So tagging a release in git is all it takes to produce the correct package version.&lt;/p&gt;
&lt;h1&gt;Step 2: the &lt;code&gt;aptblob&lt;/code&gt; tool&lt;/h1&gt;
&lt;p&gt;The real hero of this setup is &lt;a href=&quot;https://pkg.go.dev/zombiezen.com/go/aptblob&quot;&gt;&lt;code&gt;zombiezen.com/go/aptblob&lt;/code&gt;&lt;/a&gt;. It is a small Go utility that creates and maintains APT repositories without requiring you to install &lt;code&gt;reprepro&lt;/code&gt;, &lt;code&gt;aptly&lt;/code&gt;, or any other heavyweight tooling. Because it is fetched and run via &lt;code&gt;go run&lt;/code&gt;, there is no separate installation step — Go&apos;s module system handles it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;aptblob&lt;/code&gt; supports multiple storage backends via URL: &lt;code&gt;file://&lt;/code&gt; for local disk, and cloud storage providers for production hosting.&lt;/p&gt;
&lt;h1&gt;Step 3: initialising the repository&lt;/h1&gt;
&lt;p&gt;Before uploading any packages, the repository needs to be bootstrapped with an &lt;code&gt;InRelease&lt;/code&gt; file that describes the repository metadata. This is handled by &lt;code&gt;init-repo.sh&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-directive&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;go&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;zombiezen.com/go/aptblob@latest&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-k&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;KEY_ID&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;file://`&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;`/apt-repo&quot;&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;stable&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;&lt;&lt;/span&gt;&lt;span class=&quot;l-label&quot;&gt;EOF&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-string&quot;&gt;Origin: stable
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;Label: My CLI Repository
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;Suite: stable
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;Codename: stable
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;Version: 1.0
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;Architectures: arm64 amd64
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;Components: main
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;Description: The My CLI software repository
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-label&quot;&gt;EOF&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-k $KEY_ID&lt;/code&gt; flag tells &lt;code&gt;aptblob&lt;/code&gt; to sign the repository metadata with a specific GPG key. The key ID is passed via the &lt;code&gt;APT_SIGNING_KEY_ID&lt;/code&gt; environment variable so it never appears in source control. The &lt;code&gt;stable&lt;/code&gt; argument at the end is the distribution name — the string users put in their &lt;code&gt;sources.list&lt;/code&gt; entry.&lt;/p&gt;
&lt;h1&gt;Step 4: uploading the packages&lt;/h1&gt;
&lt;p&gt;With the repository initialised, the &lt;code&gt;build-apt-repo&lt;/code&gt; target uploads both &lt;code&gt;.deb&lt;/code&gt; files:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-make&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;## build-apt-repo: build the apt repository&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function&quot;&gt;build-apt-repo&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; build-deb
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    @echo &quot;Building apt repository for version &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;VERSION&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;&quot;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    @rm -rf apt-repo
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    @mkdir apt-repo
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    @KEY_ID=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;KEY_ID&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; bash init-repo.sh
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    @go run zombiezen.com/go/aptblob@latest upload -k &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;KEY_ID&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &quot;file://&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;shell&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;/apt-repo&quot; stable &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;APPNAME&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;VERSION&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;-1_arm64.deb
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    @go run zombiezen.com/go/aptblob@latest upload -k &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;KEY_ID&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;        &quot;file://&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;shell&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;/apt-repo&quot; stable &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;APPNAME&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;VERSION&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;-1_amd64.deb
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    @rm *.deb&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each &lt;code&gt;upload&lt;/code&gt; call:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copies the &lt;code&gt;.deb&lt;/code&gt; into the repository&apos;s pool directory&lt;/li&gt;
&lt;li&gt;Updates the &lt;code&gt;Packages&lt;/code&gt; index files for the relevant architecture&lt;/li&gt;
&lt;li&gt;Regenerates and re-signs the &lt;code&gt;Release&lt;/code&gt; / &lt;code&gt;InRelease&lt;/code&gt; metadata&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;code&gt;-k $(KEY_ID)&lt;/code&gt; flag is required on every upload because &lt;code&gt;aptblob&lt;/code&gt; must re-sign the repository index each time a package is added.&lt;/p&gt;
&lt;p&gt;After the upload, the temporary &lt;code&gt;.deb&lt;/code&gt; files are cleaned up — the canonical copies now live inside &lt;code&gt;apt-repo/&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Step 5: publishing&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;apt-repo/&lt;/code&gt; directory is a fully self-contained, static APT repository. Serving it is as simple as putting it behind any HTTP server or syncing it to a cloud storage bucket with public read access:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Sync to S3 (or any S3-compatible service)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;aws&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;s3&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;sync&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;./apt-repo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;s3://my-packages-bucket/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Or rsync to a VPS&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-avz&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;./apt-repo/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;user@packages.example.com:/var/www/packages/&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Why this approach works well&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;No daemon or database.&lt;/strong&gt; Traditional APT repository tools like &lt;code&gt;reprepro&lt;/code&gt; maintain a local database and run as a long-lived process. &lt;code&gt;aptblob&lt;/code&gt; is entirely stateless — the repository metadata &lt;em&gt;is&lt;/em&gt; the state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pure Go toolchain.&lt;/strong&gt; The only external dependencies are &lt;code&gt;gpg&lt;/code&gt; (for signing) and &lt;code&gt;dpkg-deb&lt;/code&gt; (for building the &lt;code&gt;.deb&lt;/code&gt; files). Everything else is managed by &lt;code&gt;go run&lt;/code&gt;, which means CI machines don&apos;t need any special APT repository software pre-installed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reproducible builds.&lt;/strong&gt; Every release starts from a clean &lt;code&gt;rm -rf apt-repo&lt;/code&gt;, so there is no risk of stale packages accumulating silently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Works with any storage.&lt;/strong&gt; Today we use &lt;code&gt;file://&lt;/code&gt; for a local build; tomorrow we can switch to an S3 or GCS URL without changing anything else.&lt;/p&gt;
&lt;h1&gt;The full release workflow&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# 1. Tag the release&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;v1.2.3&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;push&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--tags&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# 2. Build and publish&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-constant&quot;&gt;APT_SIGNING_KEY_ID&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;ABCDEF1234567890&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;make&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;build-apt-repo&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;aws&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;s3&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;sync&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;./apt-repo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;s3://my-packages-bucket/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s two commands from tag to published APT package. For a small CLI tool, it is hard to imagine a lighter-weight setup that still delivers a genuine &lt;code&gt;apt install&lt;/code&gt; experience.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it produces&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cross-compile&lt;/td&gt;
&lt;td&gt;&lt;code&gt;go build&lt;/code&gt; with &lt;code&gt;GOOS&lt;/code&gt;/&lt;code&gt;GOARCH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Linux binaries (arm64, amd64)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dpkg-deb --build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.deb&lt;/code&gt; archives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Init repo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aptblob init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Signed &lt;code&gt;InRelease&lt;/code&gt; / &lt;code&gt;Release&lt;/code&gt; metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upload packages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aptblob upload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updated &lt;code&gt;Packages&lt;/code&gt; index + pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serve&lt;/td&gt;
&lt;td&gt;Any static HTTP server&lt;/td&gt;
&lt;td&gt;Installable APT repository&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you are shipping a Go CLI and want proper APT distribution without the overhead of a full packaging pipeline, &lt;code&gt;aptblob&lt;/code&gt; plus a handful of Makefile targets gets you there in under 50 lines.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/golang&quot;&gt;#golang&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/devops&quot;&gt;#devops&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/linux&quot;&gt;#linux&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-08-14T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/hosting-your-own-apt-repository-for-a-go-cli-tool</id>
    <title>🐥 Hosting your own APT repository for a Go CLI tool</title>
    <updated>2026-08-14T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/distributing-a-go-cli-via-a-homebrew-tap"/>
    <content type="html">&lt;p&gt;Homebrew is the package manager that macOS developers reach for first. If you want your CLI tool to feel like a first-class citizen on macOS — installable with a single command, updatable with &lt;code&gt;brew upgrade&lt;/code&gt;, and auto-completed by the shell — a Homebrew tap is the right solution. This post covers how to set one up for a Go CLI, including the formula, CI validation, and the update workflow.&lt;/p&gt;
&lt;h1&gt;What is a tap?&lt;/h1&gt;
&lt;p&gt;A Homebrew &lt;em&gt;tap&lt;/em&gt; is just a GitHub repository named &lt;code&gt;homebrew-&lt;something&gt;&lt;/code&gt;. Once a user runs &lt;code&gt;brew tap org/something&lt;/code&gt;, Homebrew knows to look for formulas in that repo. From that point on, &lt;code&gt;brew install org/something/mycli&lt;/code&gt; works exactly like installing any official Homebrew package.&lt;/p&gt;
&lt;p&gt;The naming convention is the only magic: no registration, no approval process. Anyone can host a tap.&lt;/p&gt;
&lt;h1&gt;The formula&lt;/h1&gt;
&lt;p&gt;Formulas live in a &lt;code&gt;Formula/&lt;/code&gt; directory and are written in Ruby using Homebrew&apos;s DSL. For a pre-built Go binary, the formula is remarkably simple:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-ruby&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;MyCli&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Formula&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;desc&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;CLI interface for My App&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;homepage&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;https://example.com&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;https://cdn.example.com/mycli/5c7b6ef47a682ffe.../mycli-macos.tar.gz&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;1.47.0&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;sha256&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;48e146382b0f527549328da4bed9d90f2d584e1b05a8f94846cf47c3c66a9343&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;license&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;MIT&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;install&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;bin&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;mycli&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;system&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;bin&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;/mycli&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;--version&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are only four things that change on each release:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;url&lt;/code&gt; — points to the new tarball on the CDN&lt;/li&gt;
&lt;li&gt;&lt;code&gt;version&lt;/code&gt; — the semver string shown by &lt;code&gt;brew info&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sha256&lt;/code&gt; — the checksum Homebrew verifies before unpacking&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;install&lt;/code&gt; block copies the binary into the Homebrew prefix (&lt;code&gt;/opt/homebrew/bin/&lt;/code&gt; on Apple Silicon, &lt;code&gt;/usr/local/bin/&lt;/code&gt; on Intel). The &lt;code&gt;test&lt;/code&gt; block is a smoke test Homebrew runs after install — here, just confirming the binary exits cleanly when asked for its version.&lt;/p&gt;
&lt;h1&gt;Building a universal macOS binary&lt;/h1&gt;
&lt;p&gt;The URL points to a &lt;code&gt;.tar.gz&lt;/code&gt; containing a single binary. Rather than shipping separate arm64 and x86_64 tarballs and maintaining two formula entries, we use &lt;code&gt;lipo&lt;/code&gt; to merge them into one universal binary:&lt;/p&gt;
&lt;pre class=&quot;lumis&quot;&gt;&lt;code class=&quot;language-make&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;define&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;build-binary-mac&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    @echo &quot;Building for macos&quot;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    GOOS=darwin GOARCH=arm64 go build -o mycli-arm .
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    GOOS=darwin GOARCH=amd64 go build -o mycli-x86 .
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    lipo -create -output mycli mycli-x86 mycli-arm
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    tar czf ./mycli-macos.tar.gz mycli
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    rm -f mycli-x86 mycli-arm mycli
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;endef&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GOOS=darwin GOARCH=arm64 go build&lt;/code&gt; and &lt;code&gt;GOOS=darwin GOARCH=amd64 go build&lt;/code&gt; produce two binaries. &lt;code&gt;lipo -create&lt;/code&gt; stitches them into a Fat Binary that macOS automatically runs under the native architecture. The result: one tarball, one formula URL, works on both Apple Silicon and Intel Macs without any user-facing complexity.&lt;/p&gt;
&lt;h1&gt;The CDN URL strategy&lt;/h1&gt;
&lt;p&gt;The binary is uploaded to object storage (DigitalOcean Spaces in our case, but S3 or any public CDN works equally well). The URL path includes the git commit SHA:&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;l-line&quot; data-line=&quot;1&quot;&gt;https://cdn.example.com/mycli/{git-sha}/mycli-macos.tar.gz
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Embedding the commit SHA rather than the version tag means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uploads are naturally immutable — the same SHA always points to the same build&lt;/li&gt;
&lt;li&gt;Multiple release candidates for the same version cannot collide&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;create-index.py&lt;/code&gt; script (which generates a &lt;code&gt;index.json&lt;/code&gt; listing all build artifacts) uses the same SHA, keeping the release artefacts coherent&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Updating the formula&lt;/h1&gt;
&lt;p&gt;When a new version ships:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build and upload the tarball: &lt;code&gt;make build-all&lt;/code&gt; produces &lt;code&gt;mycli-macos.tar.gz&lt;/code&gt;; CI uploads it to the CDN under the new commit SHA.&lt;/li&gt;
&lt;li&gt;Compute the SHA256 of the tarball: &lt;code&gt;sha256sum mycli-macos.tar.gz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Update three lines in the formula: &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;sha256&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Open a PR against the tap repository&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The tap CI then validates the change before it merges (more on that below).&lt;/p&gt;
&lt;h1&gt;CI: validating formulas with &lt;code&gt;brew test-bot&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;The tap repo includes a GitHub Actions workflow that runs on every push and pull request:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;brew test-bot&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;pull_request&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;test-bot&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;strategy&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;      &lt;span class=&quot;l-property&quot;&gt;matrix&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;macos-latest&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;${{ matrix.os }}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Homebrew/actions/setup-homebrew@main&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test-bot&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--only-cleanup-before&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test-bot&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--only-setup&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test-bot&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--only-tap-syntax&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;test-bot&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--only-formulae&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;github.event_name == &amp;#39;pull_request&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Upload bottles as artifact&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;always() &amp;&amp; github.event_name == &amp;#39;pull_request&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;actions/upload-artifact@main&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;          &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;bottles&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;          &lt;span class=&quot;l-property&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;*.bottle.*&amp;#39;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;brew test-bot&lt;/code&gt; is Homebrew&apos;s own CI harness. The steps do progressively more:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What it checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--only-tap-syntax&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ruby syntax, formula naming conventions, &lt;code&gt;brew audit&lt;/code&gt; rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--only-formulae&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Actually installs the formula and runs the &lt;code&gt;test do&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Running on both &lt;code&gt;ubuntu-latest&lt;/code&gt; and &lt;code&gt;macos-latest&lt;/code&gt; catches issues specific to either platform — important because Homebrew has first-class Linux support (&lt;code&gt;Linuxbrew&lt;/code&gt;) and many teams use it in Docker-based CI.&lt;/p&gt;
&lt;h1&gt;CI: Merging PRs with &lt;code&gt;brew pr-pull&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;The second workflow handles the merge step:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;brew pr-pull&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;pull_request_target&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;types&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;labeled&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;pr-pull&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;contains(github.event.pull_request.labels.*.name, &amp;#39;pr-pull&amp;#39;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;ubuntu-22.04&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-property&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Homebrew/actions/setup-homebrew@main&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Homebrew/actions/git-user-config@main&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Pull bottles&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;          &lt;span class=&quot;l-property&quot;&gt;HOMEBREW_GITHUB_API_TOKEN&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;${{ github.token }}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;          &lt;span class=&quot;l-property&quot;&gt;PULL_REQUEST&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;|&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;          &lt;span class=&quot;l-function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;pr-pull&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--debug&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--tap=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;GITHUB_REPOSITORY&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;PULL_REQUEST&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Homebrew/actions/git-try-push@main&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;          &lt;span class=&quot;l-property&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;${{ github.token }}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;          &lt;span class=&quot;l-property&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;main&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;      &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Delete branch&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;github.event.pull_request.head.repo.fork == false&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;        &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;push&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--delete&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;origin&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;BRANCH&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of merging PRs normally, a maintainer applies the &lt;code&gt;pr-pull&lt;/code&gt; label. &lt;code&gt;brew pr-pull&lt;/code&gt; then:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Downloads any pre-built bottles attached to the PR as GitHub Actions artifacts&lt;/li&gt;
&lt;li&gt;Commits the bottle checksums into the formula&lt;/li&gt;
&lt;li&gt;Pushes directly to &lt;code&gt;main&lt;/code&gt; and deletes the PR branch&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This mirrors the workflow used by the official Homebrew/homebrew-core tap and means the merge is always handled by Homebrew&apos;s own tooling rather than GitHub&apos;s merge button.&lt;/p&gt;
&lt;p&gt;For a simple pre-built binary formula (no compilation step, no bottle needed), this is mostly ceremony — but it is good practice because it keeps the tap workflow consistent with the broader Homebrew ecosystem and makes it easy to add bottles later if the build-from-source path is ever needed.&lt;/p&gt;
&lt;h1&gt;The user experience&lt;/h1&gt;
&lt;p&gt;After all of this, the user-facing story is clean:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# One-time setup&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;tap&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;myorg/mycli&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Install&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;mycli&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Upgrade when a new version ships&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;brew&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;upgrade&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;mycli&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And because Homebrew auto-discovers shell completions in &lt;code&gt;share/zsh/site-functions/&lt;/code&gt;, &lt;code&gt;share/bash-completion/&lt;/code&gt;, and similar paths, completions are picked up automatically if the binary generates them into those locations at install time.&lt;/p&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;homebrew-mycli&lt;/code&gt; repo&lt;/td&gt;
&lt;td&gt;The tap — a public GitHub repo Homebrew reads formulas from&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Formula/mycli.rb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Declares the URL, version, checksum, install path, and smoke test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Universal binary (&lt;code&gt;lipo&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;One tarball covers both Apple Silicon and Intel in a single formula entry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN + commit SHA URL&lt;/td&gt;
&lt;td&gt;Immutable, content-addressed artifact storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;brew test-bot&lt;/code&gt; CI&lt;/td&gt;
&lt;td&gt;Validates formula syntax and install on every PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;brew pr-pull&lt;/code&gt; CI&lt;/td&gt;
&lt;td&gt;Homebrew-native merge flow that handles bottle attachment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The tap repository itself is under 20 lines of real content. Most of the work is a one-time setup; after that, each release is a three-line diff to the formula and a label click to merge.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.yellowduck.be/tags/golang&quot;&gt;#golang&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/mac&quot;&gt;#mac&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-08-08T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/distributing-a-go-cli-via-a-homebrew-tap</id>
    <title>🐥 Distributing a Go CLI via a Homebrew tap</title>
    <updated>2026-08-08T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/adding-2fa-to-phoenix-liveview-with-phx-gen-auth"/>
    <content type="html">&lt;p&gt;Two-factor authentication (2FA) is a great way to improve account security. This post walks through how to add TOTP-based 2FA to a Phoenix LiveView app using the built-in &lt;code&gt;phx.gen.auth&lt;/code&gt; authentication system.&lt;/p&gt;
&lt;p&gt;We’ll cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Adding required fields and dependencies&lt;/li&gt;
&lt;li&gt;2FA setup flow using LiveView&lt;/li&gt;
&lt;li&gt;TOTP challenge during login&lt;/li&gt;
&lt;li&gt;LiveView-specific login handling&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Add dependencies&lt;/h2&gt;
&lt;p&gt;Add the following libraries to &lt;code&gt;mix.exs&lt;/code&gt;:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;deps&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:nimble_totp&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;~&gt; 1.0&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:qr_code&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;~&gt; 2.2&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;deps.get&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Extend the user schema&lt;/h2&gt;
&lt;p&gt;Add two fields to your &lt;code&gt;users&lt;/code&gt; table:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# priv/repo/migrations/*_add_totp_to_users.exs&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;alter&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:users&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:totp_secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:string&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:totp_confirmed_at&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:utc_datetime&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Migrate:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;mix&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;ecto.migrate&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In your user schema:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;schema&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;users&quot;&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:totp_secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:string&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-function-call&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:totp_confirmed_at&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:utc_datetime&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-comment&quot;&gt;# ...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ensure your &lt;code&gt;changeset/2&lt;/code&gt; casts these fields:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;changeset&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;attrs&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:totp_secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:totp_confirmed_at&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;validate_required&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Add 2FA setup LiveView&lt;/h2&gt;
&lt;p&gt;Create a new LiveView at &lt;code&gt;/settings/two_factor&lt;/code&gt; that generates a secret, renders a QR code, and lets the user enter their TOTP code.&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyAppWeb.TwoFactorSetupLive&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyAppWeb&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:live_view&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;NimbleTOTP&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;QRCode&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Accounts&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;mount&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;_params&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;_session&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;current_user&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;totp_secret&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;setup?: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;      &lt;span class=&quot;l-variable&quot;&gt;secret&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;encode32&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;NimbleTOTP&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;      &lt;span class=&quot;l-variable&quot;&gt;uri&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;NimbleTOTP&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;otpauth_uri&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;MyApp:&lt;/span&gt;&lt;span class=&quot;l-string-special&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;l-string-special&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;decode32!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;issuer: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;MyApp&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;png&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;QRCode&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:png&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;      &lt;span class=&quot;l-variable&quot;&gt;b64&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;encode64&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;png&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;       &lt;span class=&quot;l-function-call&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;         &lt;span class=&quot;l-string-special-symbol&quot;&gt;setup?: &lt;/span&gt;&lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;         &lt;span class=&quot;l-string-special-symbol&quot;&gt;secret: &lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;         &lt;span class=&quot;l-string-special-symbol&quot;&gt;qr_code: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;data:image/png;base64,&lt;/span&gt;&lt;span class=&quot;l-string-special&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;b64&lt;/span&gt;&lt;span class=&quot;l-string-special&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;         &lt;span class=&quot;l-string-special-symbol&quot;&gt;code: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;         &lt;span class=&quot;l-string-special-symbol&quot;&gt;error: &lt;/span&gt;&lt;span class=&quot;l-constant-builtin&quot;&gt;nil&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;       &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;verify&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;code&quot;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;secret&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;decode32!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;32&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;NimbleTOTP&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;valid?&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;33&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;34&quot;&gt;        &lt;span class=&quot;l-module&quot;&gt;Accounts&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;update_user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;current_user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;35&quot;&gt;          &lt;span class=&quot;l-string-special-symbol&quot;&gt;totp_secret: &lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;36&quot;&gt;          &lt;span class=&quot;l-string-special-symbol&quot;&gt;totp_confirmed_at: &lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;DateTime&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;utc_now&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;37&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;38&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;39&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;40&quot;&gt;       &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;41&quot;&gt;       &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;put_flash&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:info&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;2FA enabled.&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;42&quot;&gt;       &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;push_redirect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;/settings&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;43&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;44&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;error: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Invalid code&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;45&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;46&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;47&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;!--&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt; templates/two_factor_setup_live.html.heex &lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;--&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;Two-Factor Authentication&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;%=&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;setup?&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;l-tag-delimiter&quot;&gt;%&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;Scan this QR code in your authenticator app:&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;img&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;qr_code&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-tag-delimiter&quot;&gt;/&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;phx-submit&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;Code:&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;l-tag-delimiter&quot;&gt;/&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;Verify&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;%=&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:p&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;text-red-500&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt; %&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;%&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt; %&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;2FA is already enabled.&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;%&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt; %&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Add a TOTP challenge LiveView&lt;/h2&gt;
&lt;p&gt;When a user logs in with 2FA enabled, redirect them to a LiveView at &lt;code&gt;/two_factor&lt;/code&gt; to enter their code.&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyAppWeb.TwoFactorChallengeLive&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyAppWeb&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;:live_view&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Accounts&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;NimbleTOTP&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;alias&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyAppWeb.UserAuth&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;mount&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;_params&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;pending_user_id&quot;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Accounts&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;get_user!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;code: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;error: &lt;/span&gt;&lt;span class=&quot;l-constant-builtin&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;  &lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;handle_event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;verify&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;code&quot;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;assigns&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;NimbleTOTP&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;valid?&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;decode32!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;totp_secret&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;       &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;       &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;put_flash&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:info&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;Logged in with 2FA.&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;       &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;UserAuth&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;log_in_user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;      &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;error: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;Invalid code&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;!--&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt; templates/two_factor_challenge_live.html.heex &lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;--&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;Enter your 2FA code&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;phx-submit&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;l-tag-delimiter&quot;&gt;/&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;Verify&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;%=&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;do: &lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:p&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;text-red-500&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt; %&gt;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Update login logic&lt;/h2&gt;
&lt;p&gt;After validating the user’s password (in your LiveView or controller), check if TOTP is enabled:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;totp_confirmed_at&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Phoenix.LiveView&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;put_session&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:pending_user_id&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Phoenix.LiveView&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;/two_factor&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-module&quot;&gt;UserAuth&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;log_in_user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In controllers, you’d use &lt;code&gt;put_session(conn, ...)&lt;/code&gt; and &lt;code&gt;redirect(conn, ...)&lt;/code&gt; instead.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Update &lt;code&gt;UserAuth&lt;/code&gt; helper&lt;/h2&gt;
&lt;p&gt;Make sure your &lt;code&gt;UserAuth&lt;/code&gt; module has a LiveView-compatible &lt;code&gt;log_in_user/3&lt;/code&gt;:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;log_in_user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-comment&quot;&gt;_params&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;\\&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;MyApp.Accounts&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;generate_user_session_token&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;socket&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Phoenix.LiveView&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;put_session&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:user_token&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Phoenix.LiveView&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Done&lt;/h2&gt;
&lt;p&gt;You now have a working TOTP 2FA flow in Phoenix LiveView:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Users opt in to 2FA in their settings&lt;/li&gt;
&lt;li&gt;A QR code is shown and confirmed&lt;/li&gt;
&lt;li&gt;2FA is required on next login&lt;/li&gt;
&lt;li&gt;All handled with clean LiveView flows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From here, you can extend the system with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disabling 2FA&lt;/li&gt;
&lt;li&gt;Backup codes&lt;/li&gt;
&lt;li&gt;Remembering trusted devices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let me know if you&apos;d like a follow-up on any of those features.&lt;/p&gt;&lt;p&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;a href=&quot;https://www.yellowduck.be/tags/auth&quot;&gt;#auth&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-08-03T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/adding-2fa-to-phoenix-liveview-with-phx-gen-auth</id>
    <title>🐥 Adding 2FA to Phoenix LiveView with phx.gen.auth</title>
    <updated>2026-08-03T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/finding-unexpected-http-responses-in-json-logs-with-jq"/>
    <content type="html">&lt;p&gt;When debugging an application in production, I often want a quick overview of requests that didn&apos;t result in a &quot;normal&quot; HTTP response. If your web server writes JSON logs, &lt;code&gt;jq&lt;/code&gt; makes this incredibly easy.&lt;/p&gt;
&lt;p&gt;Here&apos;s a command I regularly 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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;access_*.log&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;jq&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;      select(
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;        .status != 200 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;        .status != 206 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        .status != 302 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        .status != 304 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        .status != 308 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        .status != 101
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;      )
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;      | [.status, .request.uri]
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;      | @csv
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &amp;#39;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;uniq&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output looks something 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;l-line&quot; data-line=&quot;1&quot;&gt;404,&quot;/api/v1/users/...&quot;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;403,&quot;/admin/...&quot;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;429,&quot;/api/v1/search&quot;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;500,&quot;/api/v1/orders/...&quot;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What this does&lt;/h1&gt;
&lt;p&gt;The command performs three simple steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reads all matching access log files.&lt;/li&gt;
&lt;li&gt;Filters out the expected HTTP status codes (&lt;code&gt;200&lt;/code&gt;, &lt;code&gt;206&lt;/code&gt;, &lt;code&gt;302&lt;/code&gt;, &lt;code&gt;304&lt;/code&gt;, &lt;code&gt;308&lt;/code&gt;, and &lt;code&gt;101&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Outputs the remaining status code together with the requested URI as CSV.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, &lt;code&gt;uniq&lt;/code&gt; removes duplicate entries so you get a concise overview instead of thousands of repeated requests.&lt;/p&gt;
&lt;h1&gt;Why this is useful&lt;/h1&gt;
&lt;p&gt;This is an easy way to spot things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing routes (&lt;code&gt;404&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Authorization issues (&lt;code&gt;401&lt;/code&gt;/&lt;code&gt;403&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Rate limiting (&lt;code&gt;429&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Unexpected server errors (&lt;code&gt;500&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Any other response that deserves investigation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of searching through millions of log lines, you immediately get a list of unique problem endpoints.&lt;/p&gt;
&lt;h1&gt;A small improvement&lt;/h1&gt;
&lt;p&gt;If your logs aren&apos;t already grouped, consider sorting before calling &lt;code&gt;uniq&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;access_*.log&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;jq&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;      select(
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;        .status != 200 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;        .status != 206 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        .status != 302 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        .status != 304 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        .status != 308 and
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        .status != 101
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;      )
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;      | [.status, .request.uri]
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;      | @csv
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &amp;#39;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;uniq&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, even shorter:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;...&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;sort&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-u&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&apos;s a simple one-liner, but it&apos;s become one of my favourite ways to quickly identify unexpected responses in production logs.&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/logging&quot;&gt;#logging&lt;/a&gt; &lt;a href=&quot;https://www.yellowduck.be/tags/http&quot;&gt;#http&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-07-28T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/finding-unexpected-http-responses-in-json-logs-with-jq</id>
    <title>🐥 Finding unexpected HTTP responses in JSON logs with jq</title>
    <updated>2026-07-28T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/katagelophobia-and-cyber-security-when-fear-of-ridicule-becomes-a-vulnerability"/>
    <content type="html">&lt;p&gt;Katagelophobia — the fear of being laughed at — is rarely discussed in technical circles, yet it quietly shapes how people behave in security-critical situations. In cyber security, where human judgment is often the last line of defense, this fear can become an unexpected attack surface.&lt;/p&gt;
&lt;h1&gt;The hidden driver behind silence&lt;/h1&gt;
&lt;p&gt;Security incidents are often preceded by small signals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A suspicious email that “looks off”&lt;/li&gt;
&lt;li&gt;An unexpected MFA prompt&lt;/li&gt;
&lt;li&gt;A system behaving slightly differently than usual&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In an ideal environment, users report these signals immediately. In reality, many hesitate. Katagelophobia plays a role here: people avoid speaking up because they fear being wrong, overreacting, or being perceived as inexperienced.&lt;/p&gt;
&lt;p&gt;This creates a dangerous dynamic: attackers rely on hesitation.&lt;/p&gt;
&lt;h1&gt;Social engineering thrives on psychological pressure&lt;/h1&gt;
&lt;p&gt;Modern phishing and social engineering attacks are designed to exploit emotion, not logic. Urgency, authority, and fear are well-known tactics, but fear of embarrassment is equally powerful.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“This is urgent, don’t escalate”&lt;/li&gt;
&lt;li&gt;“Only you can fix this quickly”&lt;/li&gt;
&lt;li&gt;“Please don’t involve others yet”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These cues discourage validation and collaboration. A user already prone to avoiding ridicule is more likely to comply silently rather than question the request.&lt;/p&gt;
&lt;h1&gt;Organizational culture as a security control&lt;/h1&gt;
&lt;p&gt;Technical defenses can’t compensate for a culture where people are afraid to ask questions.&lt;/p&gt;
&lt;p&gt;Teams that unintentionally reward “knowing everything” or penalize mistakes create an environment where katagelophobia flourishes. The result:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Underreporting of incidents&lt;/li&gt;
&lt;li&gt;Delayed response times&lt;/li&gt;
&lt;li&gt;Increased dwell time for attackers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In contrast, strong security cultures normalize uncertainty:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“If in doubt, report it” is actively reinforced&lt;/li&gt;
&lt;li&gt;False positives are treated as learning opportunities&lt;/li&gt;
&lt;li&gt;Junior and non-technical staff feel safe raising concerns&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Designing systems that reduce fear&lt;/h1&gt;
&lt;p&gt;You can’t eliminate psychological traits, but you can design around them.&lt;/p&gt;
&lt;p&gt;Practical approaches:&lt;/p&gt;
&lt;h2&gt;1. Lower the cost of being wrong&lt;/h2&gt;
&lt;p&gt;Make reporting trivial and low-friction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One-click “Report phishing” buttons&lt;/li&gt;
&lt;li&gt;Dedicated Slack/Teams channels&lt;/li&gt;
&lt;li&gt;No requirement to justify suspicion&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The easier it is, the less overthinking occurs.&lt;/p&gt;
&lt;h2&gt;2. Remove judgment from feedback loops&lt;/h2&gt;
&lt;p&gt;Avoid responses like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“This was obviously safe”&lt;/li&gt;
&lt;li&gt;“You should have known this”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thank the report&lt;/li&gt;
&lt;li&gt;Explain briefly&lt;/li&gt;
&lt;li&gt;Reinforce that reporting was correct behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. Simulate safely&lt;/h2&gt;
&lt;p&gt;Phishing simulations shouldn’t shame users. If people feel tested rather than trained, katagelophobia increases.&lt;/p&gt;
&lt;p&gt;Focus on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Education over scoring&lt;/li&gt;
&lt;li&gt;Trends over individual performance&lt;/li&gt;
&lt;li&gt;Private feedback instead of public metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. Lead by example&lt;/h2&gt;
&lt;p&gt;When senior engineers or leadership openly admit uncertainty or mistakes, it sets a powerful precedent.&lt;/p&gt;
&lt;p&gt;Security improves when saying “I’m not sure” becomes acceptable.&lt;/p&gt;
&lt;h1&gt;The human layer is not optional&lt;/h1&gt;
&lt;p&gt;Cyber security discussions often focus on zero-days, encryption, and infrastructure hardening. Yet many breaches still start with a simple human interaction.&lt;/p&gt;
&lt;p&gt;Katagelophobia highlights a key reality: people don’t just fail because they lack knowledge — they fail because of social pressure.&lt;/p&gt;
&lt;p&gt;Addressing that pressure is not “soft” work. It’s a core part of building resilient systems.&lt;/p&gt;
&lt;h1&gt;Closing thought&lt;/h1&gt;
&lt;p&gt;Attackers exploit whatever works. If fear of ridicule prevents someone from reporting a suspicious email, that fear becomes part of the attack chain.&lt;/p&gt;
&lt;p&gt;Reducing that fear may be one of the simplest — and most overlooked — security improvements you can make.&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/auth&quot;&gt;#auth&lt;/a&gt;&lt;/p&gt;</content>
    <published>2026-07-25T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/katagelophobia-and-cyber-security-when-fear-of-ridicule-becomes-a-vulnerability</id>
    <title>🐥 Katagelophobia and cyber security: when fear of ridicule becomes a vulnerability</title>
    <updated>2026-07-25T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/request-input-vs-request-string-in-laravel"/>
    <content type="html">&lt;p&gt;Laravel provides several ways to retrieve input from an HTTP request. Two of the most commonly used methods are &lt;code&gt;input()&lt;/code&gt; and &lt;code&gt;string()&lt;/code&gt;. While they look similar, they have an important difference that can make your code safer and more expressive.&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;input()&lt;/code&gt; returns mixed values&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;input()&lt;/code&gt; method returns the raw value from the request.&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$name&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The return type depends entirely on the submitted data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;array&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes &lt;code&gt;input()&lt;/code&gt; the right choice when you don&apos;t know or don&apos;t care about the exact type, or when you&apos;re expecting something other than a string.&lt;/p&gt;
&lt;p&gt;For 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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$ids&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;ids&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$published&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;published&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;code&gt;string()&lt;/code&gt; always returns a Stringable object&lt;/h1&gt;
&lt;p&gt;If you expect textual input, &lt;code&gt;string()&lt;/code&gt; is often the better choice.&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$name&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of returning a plain string, it returns an &lt;code&gt;Illuminate\Support\Stringable&lt;/code&gt; instance, allowing you to immediately chain string operations.&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$username&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$request&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;username&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;lower&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No need for nested &lt;code&gt;Str::of()&lt;/code&gt; calls or temporary variables.&lt;/p&gt;
&lt;h1&gt;Better type safety&lt;/h1&gt;
&lt;p&gt;One subtle advantage is that &lt;code&gt;string()&lt;/code&gt; guarantees you&apos;re working with a string-like value.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;input()&lt;/code&gt;, it&apos;s easy to accidentally call a string function on an array:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// Potentially problematic&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$name&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;name&lt;/code&gt; unexpectedly contains an array, PHP will throw a type error.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;string()&lt;/code&gt; makes your intent explicit:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$name&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$request&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;When to use which&lt;/h1&gt;
&lt;p&gt;As a general guideline:&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;input()&lt;/code&gt; when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You expect arrays, booleans, integers, or mixed data.&lt;/li&gt;
&lt;li&gt;You simply need the raw request value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use &lt;code&gt;string()&lt;/code&gt; when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You expect text input.&lt;/li&gt;
&lt;li&gt;You plan to manipulate the value.&lt;/li&gt;
&lt;li&gt;You want more expressive, fluent code.&lt;/li&gt;
&lt;li&gt;You want to make your intent clear to future readers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;My recommendation&lt;/h1&gt;
&lt;p&gt;For textual fields like names, email addresses, search queries, slugs, or titles, prefer &lt;code&gt;string()&lt;/code&gt;. It communicates that the value is expected to be text and gives you Laravel&apos;s fluent string API for free.&lt;/p&gt;
&lt;p&gt;Reserve &lt;code&gt;input()&lt;/code&gt; for cases where the value may legitimately be another type, such as arrays from multi-select fields, boolean flags, or numeric values.&lt;/p&gt;
&lt;p&gt;It&apos;s a small change, but one that makes your codebase a little more readable, a little safer, and a little more idiomatic.&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;/p&gt;</content>
    <published>2026-07-20T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/request-input-vs-request-string-in-laravel</id>
    <title>🐥 Request::input() vs Request::string() in Laravel</title>
    <updated>2026-07-20T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/how-to-start-and-enable-clamav-service-on-linux"/>
    <content type="html">&lt;p&gt;ClamAV is a powerful, open-source antivirus engine designed to detect malware, viruses, and trojans on Linux systems. If you have just installed it, you need to start its background services so that it can protect your system and keep its virus definitions up to date.&lt;/p&gt;
&lt;p&gt;This guide will show you how to start, enable, and verify the ClamAV services using &lt;code&gt;systemctl&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Prerequisites&lt;/h1&gt;
&lt;p&gt;Before running the commands, ensure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Linux distribution installed.&lt;/li&gt;
&lt;li&gt;Administrative privileges (&lt;code&gt;sudo&lt;/code&gt; or &lt;code&gt;root&lt;/code&gt; access).&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Step 1: Start the Antivirus Daemon&lt;/h1&gt;
&lt;p&gt;The primary service is &lt;code&gt;clamav-daemon&lt;/code&gt;. This background process loads virus signatures into memory and handles on-access scanning. It also allows you to run high-speed scans using the &lt;code&gt;clamdscan&lt;/code&gt; utility.&lt;/p&gt;
&lt;p&gt;Run the following commands in your terminal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start the service:&lt;/strong&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable it to start automatically on boot:&lt;/strong&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify that it is running correctly:&lt;/strong&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;clamav-daemon&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; The daemon might take a few moments to change to an &quot;active&quot; status. This delay happens because it is loading a large database of malware definitions directly into your system&apos;s RAM.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Step 2: Start the Automatic Database Updater&lt;/h1&gt;
&lt;p&gt;An antivirus engine is only as good as its virus signatures. ClamAV uses a separate service called &lt;code&gt;clamav-freshclam&lt;/code&gt; to look for and download the latest malware definitions automatically.&lt;/p&gt;
&lt;p&gt;To get the updater running, execute these commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start the updater service:&lt;/strong&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;clamav-freshclam&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable the updater to start on boot:&lt;/strong&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;clamav-freshclam&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify the update status:&lt;/strong&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;clamav-freshclam&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Your Linux machine is now running ClamAV in the background with continuous, automated database updates. You can now safely run manual system scans or configure real-time protection.&lt;/p&gt;&lt;p&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-07-07T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/how-to-start-and-enable-clamav-service-on-linux</id>
    <title>🐥 How to start and enable ClamAV service on Linux</title>
    <updated>2026-07-07T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/fixing-korean-pdf-rendering-issues-in-poppler-on-ubuntu-22-04"/>
    <content type="html">&lt;p&gt;Poppler’s &lt;code&gt;pdftoppm&lt;/code&gt; tool is commonly used to render PDFs into images for processing, previews, or conversion pipelines. On Ubuntu 22.04, the default Poppler version (22.02) can run into issues when rendering PDFs that contain Korean text, especially when fonts are missing or incorrectly substituted.&lt;/p&gt;
&lt;p&gt;This post explains a practical fix that avoids upgrading Poppler entirely.&lt;/p&gt;
&lt;h1&gt;The problem&lt;/h1&gt;
&lt;p&gt;When rendering certain Korean PDFs with &lt;code&gt;pdftoppm&lt;/code&gt;, the output may show:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;missing characters (blank boxes or “tofu” glyphs)&lt;/li&gt;
&lt;li&gt;incorrect font substitution&lt;/li&gt;
&lt;li&gt;broken or unreadable text rendering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is often not caused by Poppler itself, but by missing font configuration and CMap data required for CJK (Chinese, Japanese, Korean) text handling.&lt;/p&gt;
&lt;h1&gt;The root cause&lt;/h1&gt;
&lt;p&gt;Poppler relies on the system font stack and external mapping data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fontconfig&lt;/strong&gt;: resolves font substitutions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CJK fonts&lt;/strong&gt;: provide actual glyphs for Korean characters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poppler CMap data&lt;/strong&gt;: maps PDF encodings to Unicode correctly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On a minimal Ubuntu server installation, two key components are often missing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;poppler-data&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CJK font packages such as Noto Sans CJK&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;The fix&lt;/h1&gt;
&lt;p&gt;Instead of upgrading Poppler, installing the correct font dependencies resolves the issue.&lt;/p&gt;
&lt;h2&gt;1. Install CJK fonts&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;fonts-noto-cjk&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;fonts-noto-core&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;fonts-unifont&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Install Poppler CMap data&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;poppler-data&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Rebuild the font cache&lt;/h2&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;fc-cache&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-fv&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Verify the fix&lt;/h1&gt;
&lt;p&gt;Re-run your rendering command:&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;pdftoppm&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;input.pdf&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;output&lt;/span&gt;
&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Korean text should now render correctly in the generated images.&lt;/p&gt;
&lt;h1&gt;Why this works&lt;/h1&gt;
&lt;p&gt;Poppler 22.x is capable of rendering CJK PDFs correctly, but only when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the system provides appropriate glyph fonts&lt;/li&gt;
&lt;li&gt;CMap mappings are available via &lt;code&gt;poppler-data&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without these, Poppler falls back to incomplete or incorrect font substitution, leading to broken output.&lt;/p&gt;
&lt;h1&gt;When you actually need a newer Poppler&lt;/h1&gt;
&lt;p&gt;Upgrading Poppler is only necessary if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rendering logic itself is broken (rare for CJK issues)&lt;/li&gt;
&lt;li&gt;you need specific bug fixes or features in newer releases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For font-related issues, upgrading is usually unnecessary and introduces avoidable dependency complexity.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;If you encounter broken Korean text rendering in &lt;code&gt;pdftoppm&lt;/code&gt; on Ubuntu 22.04, the fix is typically not a Poppler upgrade but a missing font stack.&lt;/p&gt;
&lt;p&gt;Installing &lt;code&gt;fonts-noto-cjk&lt;/code&gt; and &lt;code&gt;poppler-data&lt;/code&gt; resolves most cases immediately and keeps your system stable without rebuilding core libraries.&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/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-07-05T17:00:00Z</published>
    <id>https://www.yellowduck.be/posts/fixing-korean-pdf-rendering-issues-in-poppler-on-ubuntu-22-04</id>
    <title>🐥 Fixing Korean PDF rendering issues in Poppler on Ubuntu 22.04</title>
    <updated>2026-07-05T17:00:00Z</updated>
  </entry>
  <entry>
    <author>
      <name>Pieter Claerhout</name>
      <email>pieter@yellowduck.be</email>
    </author>
    <link rel="alternate" href="https://www.yellowduck.be/posts/the-mysql-null-safe-equality-operator"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;l-comment&quot;&gt;-- NULL&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;      &lt;span class=&quot;l-comment&quot;&gt;-- NULL&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;         &lt;span class=&quot;l-comment&quot;&gt;-- 1 (TRUE)&lt;/span&gt;
&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;deleted_at&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;l-comment&quot;&gt;-- returns nothing&lt;/span&gt;
&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;-- This silently drops rows where either column is NULL&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;orders&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;shipping_address&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;billing_address&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;l-comment&quot;&gt;-- 1 (TRUE)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;      &lt;span class=&quot;l-comment&quot;&gt;-- 0 (FALSE)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;         &lt;span class=&quot;l-comment&quot;&gt;-- 1 (TRUE)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;         &lt;span class=&quot;l-comment&quot;&gt;-- 0 (FALSE)&lt;/span&gt;
&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;-- Correctly includes rows where both columns are NULL&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;orders&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;shipping_address&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;billing_address&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-text&quot;&gt;WHERE subscriptions&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;user_id &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;!=&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; posts&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;author_id&lt;/span&gt;
&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-text&quot;&gt;NOT &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;subscriptions&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;user_id &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&lt;=&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;author_id&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;posts&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;l-variable-member&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;subscriptions&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;item_id&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;-- PostgreSQL equivalent of &lt;=&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;col &lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;DISTINCT&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; val
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-comment&quot;&gt;-- PostgreSQL equivalent of NOT (col &lt;=&gt; val)&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;col &lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;IS&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;DISTINCT&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; val&lt;/span&gt;
&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 rel="alternate" href="https://www.yellowduck.be/posts/testing-that-laravel-events-fire-after-a-transaction-commits"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;l-keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;PlaceOrderAction&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function-method&quot;&gt;__invoke&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Cart&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Order&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;transaction&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$order&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;createMany&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;            
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;            &lt;span class=&quot;l-comment&quot;&gt;// ❌ Event fired inside the transaction — listeners run&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;            &lt;span class=&quot;l-comment&quot;&gt;//    while the order row is still locked.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;            &lt;span class=&quot;l-type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;l-keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;PlaceOrderAction&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function-method&quot;&gt;__invoke&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Cart&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Order&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$order&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;transaction&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$order&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;createMany&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;lines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;l-comment&quot;&gt;// ✅ Transaction has committed — listeners can safely read/write.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Actions&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;PlaceOrderAction&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Events&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Illuminate&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Support&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Facades&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Bus&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Illuminate&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Support&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Facades&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;&lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Illuminate&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Support&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Facades&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;PHPUnit&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Framework&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-module&quot;&gt;Attributes&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Tests&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-keyword-modifier&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;l-keyword-type&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;PlaceOrderActionTest&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;TestCase&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;l-attribute&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function-method&quot;&gt;it_dispatches_order_was_placed_after_the_transaction_commits&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;        &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;Bus&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;fake&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;        &lt;span class=&quot;l-comment&quot;&gt;// Capture the DB nesting depth before the action runs.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;        &lt;span class=&quot;l-comment&quot;&gt;// LazilyRefreshDatabase / DatabaseTransactions wraps every test&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$baselineLevel&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;transactionLevel&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$levelAtDispatch&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;        &lt;span class=&quot;l-comment&quot;&gt;// Register a real listener — do NOT call Event::fake(), otherwise&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;        &lt;span class=&quot;l-comment&quot;&gt;// the dispatcher is replaced with a mock and no listeners run.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$levelAtDispatch&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$levelAtDispatch&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;transactionLevel&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$cart&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Cart&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;withLines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;32&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;33&quot;&gt;        &lt;span class=&quot;l-function-call&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;PlaceOrderAction&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;34&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;35&quot;&gt;        &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;36&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$baselineLevel&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;37&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$levelAtDispatch&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;38&quot;&gt;            &lt;span class=&quot;l-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;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;39&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;40&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;41&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-type&quot;&gt;Action&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;fake&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;StartFulfillmentAction&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$levels&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;InventoryReserved&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;InvoiceQueued&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-type&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;transactionLevel&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;PlaceOrderAction&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$cart&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;&lt;span class=&quot;l-keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;array_keys&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;assertEquals&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$baselineLevel&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$eventClass&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt; must be dispatched outside the transaction.&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;// Also assert every expected event actually fired.&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;assertSame&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;OrderWasPlaced&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;InventoryReserved&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;InvoiceQueued&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;array_keys&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$levels&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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 rel="alternate" href="https://www.yellowduck.be/posts/speeding-up-s3-uploads-in-github-actions-with-bash-parallelism"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Upload to CDN&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;|&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;public/build&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--acl-public&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;public/img&lt;/span&gt;   &lt;span class=&quot;l-variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--acl-public&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;public/js&lt;/span&gt;    &lt;span class=&quot;l-variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;Upload to CDN&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-property&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;|&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;public/build&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--acl-public&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;public/img&lt;/span&gt;   &lt;span class=&quot;l-variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--acl-public&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;public/js&lt;/span&gt;    &lt;span class=&quot;l-variable-parameter&quot;&gt;s3://my-bucket/assets/&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--recursive&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--acl-public&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;pid&lt;/span&gt; &lt;span class=&quot;l-keyword-conditional&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-character-special&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;l-keyword-repeat&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;wait&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;l-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;${pids[@]}&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Dangerous — only checks the exit code of the last job&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;public/build&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;public/img&lt;/span&gt;   &lt;span class=&quot;l-variable-parameter&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;s3cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;put&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;public/js&lt;/span&gt;    &lt;span class=&quot;l-variable-parameter&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;some-command&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;arg1&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;some-command&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;arg2&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;some-command&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;arg3&lt;/span&gt; &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;&amp;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;pid&lt;/span&gt; &lt;span class=&quot;l-keyword-conditional&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;pids&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-character-special&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;l-keyword-repeat&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;wait&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;l-function-builtin&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;l-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 rel="alternate" href="https://www.yellowduck.be/posts/debugging-phpunit-notices-in-laravel-parallel-tests"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-constant&quot;&gt;LARAVEL_PARALLEL_TESTING&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;vendor/brianium/paratest/bin/paratest&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;--colors=always&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;--configuration=/path/to/phpunit.xml&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;--runner=\\Illuminate\\Testing\\ParallelRunner&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;--display-phpunit-notices&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;--fail-on-all-issues&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-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-plaintext&quot; translate=&quot;no&quot; tabindex=&quot;0&quot;&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;1&quot;&gt;NOTICE  tests/Unit/Services/OrderServiceTest.php:42
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;Method Illuminate\Testing\Assert::assertContains() is deprecated. Use assertStringContainsString() instead.
&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-comment&quot;&gt;# Start with one file&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-constant&quot;&gt;LARAVEL_PARALLEL_TESTING&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;vendor/brianium/paratest/bin/paratest&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;--runner=\\Illuminate\\Testing\\ParallelRunner&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;--display-phpunit-notices&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;--fail-on-all-issues&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;l-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 rel="alternate" href="https://www.yellowduck.be/posts/from-n-1-to-1-extracting-paged-pdf-text-with-a-single-pdftotext-call"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;pdftotext&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;document.pdf&lt;/span&gt; &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;getPagedTextFromFile&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Collection&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;$pagedText&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;$pageCount&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;pdfInfoService&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getPageCountFromFile&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-keyword-repeat&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$pageCount&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getRawTextFromFile&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;pdftotext&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;document.pdf&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-P&lt;/span&gt; &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;getPagedTextFromFile&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type-builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type&quot;&gt;Collection&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;$pagedText&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-keyword-exception&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;mimeTypeForPath&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;application/pdf&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;            &lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$result&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;runExternalProcess&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;            &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;pdftools.pdftotext&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$path&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;            &lt;span class=&quot;l-number&quot;&gt;180&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getStdErr&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;            &lt;span class=&quot;l-keyword-exception&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Exception&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getStdErr&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;        &lt;span class=&quot;l-comment&quot;&gt;// pdftotext separates pages with \f; rtrim strips any optional trailing \f&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;        &lt;span class=&quot;l-variable&quot;&gt;$pages&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;explode&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\f&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;rtrim&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$result&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getStdOut&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\f&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;        &lt;span class=&quot;l-keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$pages&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$pageText&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$pageText&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot; &lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\t&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\r&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\x0B&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;27&quot;&gt;            &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;28&quot;&gt;                &lt;span class=&quot;l-variable&quot;&gt;$lines&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;splitInLines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$pageText&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;29&quot;&gt;                &lt;span class=&quot;l-keyword-conditional&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$lines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&amp;&amp;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;preg_match&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;DOCUSIGN_HEADER_PATTERN&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$lines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;30&quot;&gt;                    &lt;span class=&quot;l-function-call&quot;&gt;array_shift&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$lines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;31&quot;&gt;                    &lt;span class=&quot;l-variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;implode&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$lines&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot; &lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\t&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\r&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;l-string-escape&quot;&gt;\x0B&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;32&quot;&gt;                &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;33&quot;&gt;            &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;34&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;35&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-keyword-conditional-ternary&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$pageText&lt;/span&gt; &lt;span class=&quot;l-keyword-conditional-ternary&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-constant-builtin&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;36&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;37&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-keyword-exception&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;38&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;Log&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;pdftotext | &lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getMessage&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;39&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;40&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;41&quot;&gt;    &lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$pagedText&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;42&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&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 rel="alternate" href="https://www.yellowduck.be/posts/the-silent-saboteur-how-quiet-can-break-your-laravel-command-sequences"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;int&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;$commandsToRun&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;SomeCommand&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;        &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;AnotherCommand&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;     &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;DatabasePruneCommand&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;--quiet&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;MoreCommands&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;       &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;FinalCommand&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;       &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$commandsToRun&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$commandToRun&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$parameters&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;        &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$commandToRun&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;        &lt;span class=&quot;l-type&quot;&gt;Artisan&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$commandToRun&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$parameters&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;    &lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$output&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;setVerbosity&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;OutputInterface&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;VERBOSITY_QUIET&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-repeat&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$commandsToRun&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$commandToRun&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$parameters&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$commandToRun&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;$verbosity&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getVerbosity&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-type&quot;&gt;Artisan&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$commandToRun&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;$parameters&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;setVerbosity&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$verbosity&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-modifier&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;it_restores_verbosity_so_quiet_commands_do_not_suppress_subsequent_output&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;void&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;// Simulate a sub-command that sets quiet mode&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-type&quot;&gt;Artisan&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;shouldReceive&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;call&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;twice&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;        &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;andReturnUsing&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword-import&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$output&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;            &lt;span class=&quot;l-variable&quot;&gt;$output&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;setVerbosity&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;OutputInterface&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;VERBOSITY_QUIET&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;            &lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;// Both titles must still appear&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;$output&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;expects&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;exactly&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;title&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;subject&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;runSequenceOfCommands&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;$command&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;first:command&amp;#39;&lt;/span&gt;  &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;second:command&amp;#39;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&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 rel="alternate" href="https://www.yellowduck.be/posts/how-to-check-how-much-memory-a-systemd-unit-is-actually-using"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;Memory: 423.1M
&lt;/div&gt;&lt;div class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;l-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;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--property=MemoryCurrent&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--value&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&amp;#39;{ printf &quot;%.2f MB\n&quot;, $1 / 1024 / 1024 }&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;l-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;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;ps&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;pid,rss,vsz,cmd&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-string-special-path&quot;&gt;PID&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;watch&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt; \
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-markup-heading&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-property&quot;&gt;MemoryMax&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;daemon-reload&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;restart&lt;/span&gt; &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;my-service.service&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-function-call&quot;&gt;systemctl&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;my-service.service&lt;/span&gt; &lt;span class=&quot;l-variable-parameter&quot;&gt;--property=MemoryCurrent&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-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 rel="alternate" href="https://www.yellowduck.be/posts/fixing-phpunit-13-with-without-expects-deprecations-in-laravel-tests"/>
    <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;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$mock&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;expects&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;createPartialMock&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;createPartialMock&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getMockBuilder&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;onlyMethods&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getMock&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$class&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getMockBuilder&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;onlyMethods&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-string&quot;&gt;&amp;#39;confirm&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getMock&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;$class&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;expects&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&amp;#39;runSequenceOfCommands&amp;#39;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;        &lt;span class=&quot;l-variable-builtin&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;isInstanceOf&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-type&quot;&gt;NightlyCommand&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;            &lt;span class=&quot;l-comment&quot;&gt;// expected commands&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;        &lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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 rel="alternate" href="https://www.yellowduck.be/posts/why-crypto-getrandomvalues-matters-in-javascript"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;36&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-keyword-return&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;byte&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;padStart&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;0&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;generateToken&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;&lt;span class=&quot;l-keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Uint16Array&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Int32Array&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;bytes&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getRandomValues&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;bytes&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;crypto&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;randomBytes&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;randomUUID&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-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;l-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 rel="alternate" href="https://www.yellowduck.be/posts/how-to-find-all-jira-issues-ever-assigned-to-someone-even-historical-ones"/>
    <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;l-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;l-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;l-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;l-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;l-line&quot; data-line=&quot;1&quot;&gt;project = &quot;PLATFORM&quot;
&lt;/div&gt;&lt;div class=&quot;l-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;l-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;l-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 rel="alternate" href="https://www.yellowduck.be/posts/downloading-external-images-as-squares-from-a-phoenix-app"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-keyword-function&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;l-function&quot;&gt;image_proxy&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;url&quot;&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;l-boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;allowed_url?&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;       &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;&lt;-&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Req&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-keyword&quot;&gt;do&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;content_type&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;l-variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;headers&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;      &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;content-type&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;image/jpeg&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;      &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;      &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-module&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;;&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;      &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;hd&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;put_resp_content_type&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;content_type&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-operator&quot;&gt;|&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;else&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;400&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;Invalid URL&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;    &lt;span class=&quot;l-comment&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;l-function-call&quot;&gt;send_resp&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;502&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;Failed to fetch image&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;end&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-variable-builtin&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;phx:download-square-image&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;filename&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;detail&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;proxyUrl&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;`/image-proxy?url=&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;l-function-builtin&quot;&gt;encodeURIComponent&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;  &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;img&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-keyword-operator&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;l-constructor&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method&quot;&gt;onload&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-type-builtin&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;naturalWidth&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;naturalHeight&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;canvas&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;canvas&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;width&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;height&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;11&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;12&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;2d&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;13&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;offsetX&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;naturalWidth&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;14&quot;&gt;    &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;offsetY&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;naturalHeight&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;15&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;offsetX&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;offsetY&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;16&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;17&quot;&gt;    &lt;span class=&quot;l-variable&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;toBlob&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable-parameter&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;18&quot;&gt;      &lt;span class=&quot;l-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable-builtin&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;19&quot;&gt;      &lt;span class=&quot;l-variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;href&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-constant&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;createObjectURL&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;20&quot;&gt;      &lt;span class=&quot;l-variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;download&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;21&quot;&gt;      &lt;span class=&quot;l-variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;22&quot;&gt;      &lt;span class=&quot;l-constant&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-method-call&quot;&gt;revokeObjectURL&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;23&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string&quot;&gt;&quot;image/jpeg&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-number&quot;&gt;0.95&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;24&quot;&gt;  &lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;25&quot;&gt;  &lt;span class=&quot;l-variable&quot;&gt;img&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-variable-member&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;l-variable&quot;&gt;proxyUrl&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;26&quot;&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  &lt;span class=&quot;l-tag-attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;cursor-pointer&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;  &lt;span class=&quot;l-tag-attribute&quot;&gt;phx-click&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;{&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;span class=&quot;l-module&quot;&gt;JS&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;l-function-call&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;phx:download-square-image&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;      &lt;span class=&quot;l-string-special-symbol&quot;&gt;detail: &lt;/span&gt;&lt;span class=&quot;l-punctuation-special&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;l-string-special-symbol&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;l-constant&quot;&gt;image_url&lt;/span&gt;&lt;span class=&quot;l-punctuation-delimiter&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;l-string-special-symbol&quot;&gt;filename: &lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;image.jpg&quot;&lt;/span&gt;&lt;span class=&quot;l-punctuation-bracket&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;span class=&quot;l-punctuation-bracket&quot;&gt;)&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;  &lt;span class=&quot;l-tag-delimiter&quot;&gt;}&lt;/span&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;9&quot;&gt;  Download
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;10&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;l-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 rel="alternate" href="https://www.yellowduck.be/posts/the-tailwind-enabled-selector-trick-for-disabled-buttons"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;bg-gray-200 hover:bg-gray-300 hover:cursor-pointer&quot;&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  Click me
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;l-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;l-string&quot;&gt;&quot;bg-gray-200 enabled:hover:bg-gray-300 enabled:hover:cursor-pointer&quot;&lt;/span&gt; &lt;span class=&quot;l-tag-attribute&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;l-text&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;  Click me
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&quot;&gt;&lt;/&lt;/span&gt;&lt;span class=&quot;l-tag&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;l-tag-delimiter&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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;&lt;span class=&quot;l-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 rel="alternate" href="https://www.yellowduck.be/posts/hide-a-layout-section-from-a-specific-child-view-in-laravel-blade"/>
    <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;l-line&quot; data-line=&quot;1&quot;&gt;{{-- resources/views/layouts/login.blade.php --}}
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;@php $loginImage = Contractify::loginImage(); @endphp
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;@if ($loginImage-&gt;src)
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    &lt;div class=&quot;...&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;        &lt;img src=&quot;{{ $loginImage-&gt;src }}&quot; alt=&quot;{{ $loginImage-&gt;alt }}&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;    &lt;/div&gt;
&lt;/div&gt;&lt;div class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;{{-- resources/views/layouts/login.blade.php --}}
&lt;/div&gt;&lt;div class=&quot;l-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;l-line&quot; data-line=&quot;3&quot;&gt;    @php $loginImage = Contractify::loginImage(); @endphp
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;    @if ($loginImage-&gt;src)
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;        &lt;div class=&quot;...&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;            &lt;img src=&quot;{{ $loginImage-&gt;src }}&quot; alt=&quot;{{ $loginImage-&gt;alt }}&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;        &lt;/div&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;8&quot;&gt;    @endif
&lt;/div&gt;&lt;div class=&quot;l-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;l-line&quot; data-line=&quot;1&quot;&gt;{{-- resources/views/vendor/passport/authorize.blade.php --}}
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;2&quot;&gt;@extends(&amp;#39;layouts.login&amp;#39;)
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;3&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;4&quot;&gt;@section(&amp;#39;hide-login-image&amp;#39;)@endsection
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;5&quot;&gt;
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;6&quot;&gt;@section(&amp;#39;content&amp;#39;)
&lt;/div&gt;&lt;div class=&quot;l-line&quot; data-line=&quot;7&quot;&gt;    {{-- ... --}}
&lt;/div&gt;&lt;div class=&quot;l-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>
</feed>