<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Secrets on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/secrets/</link><description>Recent content in Secrets on Guillaume Delré</description><generator>Hugo</generator><language>en</language><lastBuildDate>Thu, 14 May 2026 15:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/tags/secrets/index.xml" rel="self" type="application/rss+xml"/><item><title>What Survives the Build</title><link>https://guillaumedelre.github.io/2026/05/14/what-survives-the-build/</link><pubDate>Thu, 14 May 2026 15:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2026/05/14/what-survives-the-build/</guid><description>Part 2 of 8 in &amp;quot;Symfony to the Cloud: Twelve Factors, Thirteen Services&amp;quot;: How committed .env files feed a build step that bakes credentials into Docker layers — and what it takes to empty the file down to four lines.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>At some point during a cloud migration audit, someone ran this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run --rm &lt;image&gt; php -r <span style="color:#e6db74">&#34;var_dump(require &#39;.env.local.php&#39;);&#34;</span>
</span></span></code></pre></div><p>The output showed everything that <code>composer dump-env prod</code> had compiled into the image at build time. Which meant it showed everything that had been in the <code>.env</code> file when the image was built. Which meant it showed these, among others:</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">INFLUXDB_INIT_ADMIN_TOKEN=&lt;influxdb-admin-token&gt;
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=admin123
BLACKFIRE_CLIENT_ID=&lt;blackfire-client-id&gt;
BLACKFIRE_CLIENT_TOKEN=&lt;blackfire-client-token&gt;
BLACKFIRE_SERVER_ID=&lt;blackfire-server-id&gt;
BLACKFIRE_SERVER_TOKEN=&lt;blackfire-server-token&gt;
NGROK_AUTHTOKEN=replace-me-optionnal
</code></pre><p>Twenty-five variables in total. Every credential that had accumulated in the root <code>.env</code> over three years, now permanent in an image layer.</p>
<h2 id="how-dump-env-works">How <code>dump-env</code> works</h2>
<p><code>composer dump-env prod</code> is a legitimate Symfony optimization. Instead of parsing <code>.env</code> files on every request, the runtime loads a pre-compiled PHP array from <code>.env.local.php</code>. Faster and simpler.</p>
<p>The problem is what it reads. The Dockerfile copies the repository into the image with <code>COPY . ./</code>, <code>.env</code> included. Then <code>dump-env prod</code> reads that file and compiles every variable into <code>.env.local.php</code>. The image ships with a frozen snapshot of the credentials that were in <code>.env</code> at build time.</p>
<p>Docker layers are immutable archives. Even if a subsequent step removed <code>.env</code> from the container filesystem, the layer containing it would still exist inside the image. <code>docker save &lt;image&gt;</code> produces a tarball of every layer; extracting any file from any point in the build history is straightforward. The credentials are invisible at runtime. They are not gone.</p>
<p><a href="https://12factor.net/build-release-run" target="_blank" rel="noopener noreferrer">Factor V</a>
 calls this out directly: a build artifact should be environment-agnostic, with config arriving at the release step from outside. Once credentials are compiled in, the image is no longer portable. You can&rsquo;t promote it across environments. You build twice and hope the second build behaves like the first.</p>
<h2 id="how-twenty-five-variables-accumulate">How twenty-five variables accumulate</h2>
<p>Before tracing how this gets fixed, it&rsquo;s worth understanding how it happened.</p>
<p>The <code>BLACKFIRE_*</code> tokens are the easy case to understand. A team member sets up profiling, needs to share the configuration, and the repository is already open to everyone. One line in <code>.env</code> is the path of least resistance. The InfluxDB and Grafana credentials follow the same logic — shared tooling, shared repo, one commit.</p>
<p>Then there are the variables that reveal a different kind of drift. In some of the service-level <code>.env</code> files:</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">APP__RATINGS__SERIALS=&#39;{&#34;brand1&#34;:{&#34;fr&#34;:&#34;12345&#34;},...}&#39;  # ~40 lines of JSON
APP__YOUTUBE__CREDENTIALS=&#39;{&#34;brand1&#34;:{&#34;client_id&#34;:&#34;xxx&#34;,&#34;refresh_token&#34;:&#34;yyy&#34;},...}&#39;
</code></pre><p>Audience measurement serial numbers. YouTube API refresh tokens per brand. These aren&rsquo;t secrets in the Blackfire sense. They&rsquo;re business data — the kind of values that vary between brands and environments, that someone decided to version in <code>.env</code> because they behaved like configuration and <code>.env</code> was where configuration lived.</p>
<p>Twenty-five variables is the sum of incremental decisions, none of which felt wrong in isolation. The problem is structural: when <code>.env</code> is the only answer available, everything starts looking like it belongs there.</p>
<h2 id="where-things-actually-belong">Where things actually belong</h2>
<p>Emptying the file required answering one question for each variable: <em>where does this actually belong?</em></p>
<p>The answers revealed three categories that the team had never explicitly named:</p>
<p><strong>Static config</strong> lives in code. Business rules, routing logic, Symfony parameter files — anything that doesn&rsquo;t vary between deployments. A change requires a rebuild. The JSON blobs for audience measurement serials turned out not to be static config at all: they were queried from a dedicated Config service at runtime. They had no business being in a file.</p>
<p><strong>Environment config</strong> varies between deployments: hostnames, connection strings, third-party credentials. This is what <a href="https://12factor.net/config" target="_blank" rel="noopener noreferrer">Factor III</a>
 means by &ldquo;config in environment variables&rdquo; — real OS-level variables injected by the runtime, never files that travel with the code. In Kubernetes, this becomes a ConfigMap for non-sensitive values and a Kubernetes Secret for credentials. The choice for secrets management was SOPS — credentials are encrypted and committed to git, rather than stored in an external vault like Azure Key Vault or HashiCorp Vault. A vault trades simplicity for auditability: automatic rotation, centralized audit logs, workload identity-based access with no key to protect. SOPS trades those capabilities for a simpler operational model — no external service to query at deploy time, secrets travel through the normal code review process, git history serves as the audit trail. The accepted downsides are manual rotation and the responsibility of protecting the decryption key itself. For the team&rsquo;s scale, the tradeoff was deliberate.</p>
<p><strong>Dynamic config</strong> changes without a deployment: editorial parameters, per-brand thresholds, content moderation settings. It belongs in a database, managed through the application&rsquo;s Config service. Some of what had accumulated in <code>.env</code> files was this category all along, passing as static defaults because it changed rarely enough that nobody noticed.</p>
<p>Once the categories had names, the variables sorted themselves. The root <code>.env</code> ended at four lines:</p>
<pre tabindex="0"><code class="language-dotenv" data-lang="dotenv">DOMAIN=platform.127.0.0.1.sslip.io
XDEBUG_MODE=off
SERVER_NAME=:80
APP_ENV=dev
</code></pre><p>Safe defaults. Nothing sensitive. <code>dump-env prod</code> now compiles empty strings; real values arrive at runtime from Kubernetes.</p>
<h2 id="the-postgresql-image">The PostgreSQL image</h2>
<p>The PostgreSQL image used in CI has a hardcoded password:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">postgres:15</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">ENV</span> POSTGRES_PASSWORD<span style="color:#f92672">=</span>admin123
</span></span></code></pre></div><p>This looks like the same problem. It isn&rsquo;t, because the threat model is different. The CI database is ephemeral — it exists for the duration of a pipeline run, contains no real data, and runs in an isolated network. A hardcoded password on a throwaway test database is an acceptable risk, not a policy exception.</p>
<p>In production, the question doesn&rsquo;t arise: the platform uses Azure Flexible Server, a managed PostgreSQL service. There is no Docker image. Credentials arrive via Helm chart injection, never touching a layer.</p>
<h2 id="what-survives-the-build-now">What survives the build now</h2>
<p>The image that ships to production now contains a guarantee: <code>var_dump(require '.env.local.php')</code> returns only empty strings and safe defaults. The credentials aren&rsquo;t there because they were never put there — they arrive at runtime, from outside.</p>
<p>That&rsquo;s the responsibility boundary <code>dump-env</code> had been quietly erasing: the image is the application, the runtime is the environment. They should not know each other&rsquo;s secrets.</p>
]]></content:encoded></item><item><title>Symfony 5.0: String, Notifier, and the secrets vault</title><link>https://guillaumedelre.github.io/2020/01/06/symfony-5.0-string-notifier-and-the-secrets-vault/</link><pubDate>Mon, 06 Jan 2020 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2020/01/06/symfony-5.0-string-notifier-and-the-secrets-vault/</guid><description>Part 5 of 11 in &amp;quot;Symfony Releases&amp;quot;: Symfony 5.0 adds a Unicode-aware String component, a multi-channel Notifier, and a built-in secrets vault.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 5.0 released November 21, 2019, same day as 4.4. Where 4.4 is about stability and a long support window, 5.0 is the next chapter: no deprecated code, PHP 7.2.5 minimum, and a handful of new components that finally address gaps that had piled up for years.</p>
<h2 id="the-string-component">The String component</h2>
<p>PHP&rsquo;s string handling is famously scattered: prefix-style functions here (<code>str_</code>), suffix-style there (<code>strpos</code>), inconsistent encoding support, and nothing object-oriented in sight. The String component wraps all of this into a fluent, unicode-aware object API:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Component\String\UnicodeString</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$str <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">UnicodeString</span>(<span style="color:#e6db74">&#39;  Hello World  &#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> $str<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">trim</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">lower</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">&#39; &#39;</span>, <span style="color:#e6db74">&#39;-&#39;</span>); <span style="color:#75715e">// hello-world
</span></span></span></code></pre></div><p>The practical addition is the <code>Slugger</code>, a locale-aware slug generator that actually handles accented characters correctly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$slug <span style="color:#f92672">=</span> $slugger<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">slug</span>(<span style="color:#e6db74">&#39;L\&#39;été à Montréal&#39;</span>); <span style="color:#75715e">// l-ete-a-montreal
</span></span></span></code></pre></div><p>Before, you&rsquo;d pull in a third-party library or write your own. Now it ships with FrameworkBundle, available by default.</p>
<h2 id="notifier">Notifier</h2>
<p>Email is handled by Mailer. SMS, push notifications, chat messages: no first-party story, until now. The Notifier component adds one: a unified interface over dozens of channels and providers.</p>
<p>The same notification can hit Slack, trigger an SMS via Twilio, or end up as a push notification, all configured through DSNs. Adding a new channel is a config change, not a code change.</p>
<h2 id="secrets-vault">Secrets vault</h2>
<p>Storing secrets in <code>.env</code> files works, but the values are plain text, shared environments are a pain, and there&rsquo;s no native way to encrypt anything at rest.</p>
<p>Symfony 5.0 adds a <code>secrets:</code> command family and a vault mechanism. Secrets are encrypted with a key pair stored outside the repository. The encrypted files get committed; the decrypt key does not. In production, the key comes in as an environment variable or gets injected from a secret manager.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>php bin/console secrets:set DATABASE_PASSWORD
</span></span><span style="display:flex;"><span>php bin/console secrets:decrypt-to-local --force
</span></span></code></pre></div><p>Not a full-blown secrets management solution, but a real step up from a plain <code>.env</code> file sitting unencrypted in your repo.</p>
<h2 id="mailer-gets-a-notification-layer">Mailer gets a notification layer</h2>
<p>The Mailer component arrived in 4.4. What 5.0 adds on top is the <code>NotificationEmail</code> — a pre-styled, responsive email built on Foundation for Emails, with an explicit API for importance levels and call-to-action buttons:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Symfony\Bridge\Twig\Mime\NotificationEmail</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$email <span style="color:#f92672">=</span> (<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">NotificationEmail</span>())
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">from</span>(<span style="color:#e6db74">&#39;alerts@example.com&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">to</span>(<span style="color:#e6db74">&#39;admin@example.com&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">subject</span>(<span style="color:#e6db74">&#39;Disk usage critical&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">markdown</span>(<span style="color:#e6db74">&#39;The disk on **prod-01** is at 94%. Check it now.&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">action</span>(<span style="color:#e6db74">&#39;Open dashboard&#39;</span>, <span style="color:#e6db74">&#39;https://example.com/servers&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">importance</span>(<span style="color:#a6e22e">NotificationEmail</span><span style="color:#f92672">::</span><span style="color:#a6e22e">IMPORTANCE_URGENT</span>);
</span></span></code></pre></div><p>No template to write, no inline CSS to wrestle with. For transactional alerts, billing notifications, and system emails, it covers 80% of what you need without touching anything.</p>
<h2 id="lazy-firewalls-and-the-caching-problem">Lazy firewalls and the caching problem</h2>
<p>Every stateful firewall in Symfony loads the user from session on every request, whether the action needs it or not. Which means any response is uncacheable by default, even for pages that never touch <code>$this-&gt;getUser()</code>.</p>
<p>5.0 adds <code>lazy</code> mode for firewalls, which defers session access until the code actually calls <code>is_granted()</code> or reaches for the user token:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># config/packages/security.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">security</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">firewalls</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">main</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">pattern</span>: <span style="color:#ae81ff">^/</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">anonymous</span>: <span style="color:#ae81ff">lazy</span>
</span></span></code></pre></div><p>Pages that don&rsquo;t need the user become cacheable again. New projects get this by default via the Flex recipe; existing ones need a one-line config change.</p>
<h2 id="password-migrations-without-the-big-bang">Password migrations without the big bang</h2>
<p>Migrating a live app from bcrypt to argon2id used to mean forcing a password reset on every user. The <code>PasswordUpgraderInterface</code> makes it gradual: at login, Symfony checks whether the stored hash matches the current algorithm. If not, it rehashes on the spot and calls your upgrader to save it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// src/Repository/UserRepository.php
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">UserRepository</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">ServiceEntityRepository</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">PasswordUpgraderInterface</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">upgradePassword</span>(<span style="color:#a6e22e">UserInterface</span> $user, <span style="color:#a6e22e">string</span> $newHashedPassword)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        $user<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">setPassword</span>($newHashedPassword);
</span></span><span style="display:flex;"><span>        $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getEntityManager</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">flush</span>();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Pair that with <code>algorithm: auto</code> in the encoder config, and old hashes migrate silently as users log in. No migration script, no downtime, no user friction.</p>
<h2 id="errorhandler-replaces-debug">ErrorHandler replaces Debug</h2>
<p>The Debug component is gone. Its replacement, ErrorHandler, does the same job (converting PHP errors to exceptions, showing nice error pages) but without requiring Twig. For API apps that never render HTML, that matters: ErrorHandler generates errors in the format of the request (JSON, XML, plain text) following RFC 7807:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;title&#34;</span>: <span style="color:#e6db74">&#34;Not Found&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#ae81ff">404</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;detail&#34;</span>: <span style="color:#e6db74">&#34;Sorry, the page you are looking for could not be found&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The routing config moves from <code>TwigBundle</code> to <code>FrameworkBundle</code>, and that&rsquo;s the only migration step for most projects. One line, done.</p>
<h2 id="event-listeners-finally-less-verbose">Event listeners, finally less verbose</h2>
<p>Registering a kernel event listener used to mean explicitly naming the event in the service tag. Symfony 5.0 infers it from the method signature:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// No tag configuration needed beyond kernel.event_listener
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SecurityListener</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">onKernelRequest</span>(<span style="color:#a6e22e">RequestEvent</span> $event)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Symfony reads the type hint and figures out the event
</span></span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># config/services.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">App\EventListener\SecurityListener</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">tags</span>: [<span style="color:#ae81ff">kernel.event_listener]</span>
</span></span></code></pre></div><p>Use <code>__invoke()</code> and it works the same way. Bulk-register a whole directory of listeners with one resource block, and Symfony figures out which event each one handles.</p>
<h2 id="httpclient-grows-up">HttpClient grows up</h2>
<p>The HttpClient component arrived in 4.4 as stable. 5.0 adds a few useful things on top:</p>
<p>NTLM authentication for corporate environments, conditional buffering via a callback (buffer large responses only when the content-type matches), a <code>max_duration</code> option that caps the total request time regardless of network conditions, and <code>toStream()</code> to turn any response into a standard PHP stream for code that expects <code>fread()</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$response <span style="color:#f92672">=</span> $client<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">request</span>(<span style="color:#e6db74">&#39;GET&#39;</span>, <span style="color:#e6db74">&#39;https://api.example.com/large-export&#39;</span>, [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;max_duration&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">30.0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;buffer&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">fn</span>(<span style="color:#66d9ef">array</span> $headers)<span style="color:#f92672">:</span> <span style="color:#a6e22e">bool</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">str_contains</span>($headers[<span style="color:#e6db74">&#39;content-type&#39;</span>][<span style="color:#ae81ff">0</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;&#39;</span>, <span style="color:#e6db74">&#39;json&#39;</span>),
</span></span><span style="display:flex;"><span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Stream it instead of loading it all into memory
</span></span></span><span style="display:flex;"><span>$stream <span style="color:#f92672">=</span> $response<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">toStream</span>();
</span></span></code></pre></div><p>The client also got full interoperability with PSR-18 and HTTPlug v1/v2, so any library that depends on those abstractions just works with it.</p>
<h2 id="what-50-removes">What 5.0 removes</h2>
<p>5.0 drops everything deprecated in 4.4. The most notable:</p>
<ul>
<li><code>WebServerBundle</code> (use <code>symfony server:start</code> from the CLI tool instead)</li>
<li>The old security system&rsquo;s <code>AnonymousToken</code> (replaced by <code>NullToken</code>)</li>
<li>Old form event names</li>
<li>Symfony&rsquo;s internal ClassLoader</li>
<li>The Debug component (replaced by ErrorHandler)</li>
</ul>
<p>If you ran your 4.4 app with deprecation notices active and fixed the warnings, upgrading to 5.0 requires no code changes.</p>
]]></content:encoded></item></channel></rss>