<?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>Http-Client on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/http-client/</link><description>Recent content in Http-Client on Guillaume Delré</description><generator>Hugo</generator><language>en</language><lastBuildDate>Fri, 15 May 2026 15:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/tags/http-client/index.xml" rel="self" type="application/rss+xml"/><item><title>The Host That Hid the Graph</title><link>https://guillaumedelre.github.io/2026/05/15/the-host-that-hid-the-graph/</link><pubDate>Fri, 15 May 2026 15:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2026/05/15/the-host-that-hid-the-graph/</guid><description>Part 4 of 8 in &amp;quot;Symfony to the Cloud: Twelve Factors, Thirteen Services&amp;quot;: Thirteen services sharing six identical gateway variables. The config looked simple. The dependency graph was invisible — until Kubernetes asked where each service actually lived.</description><category>symfony-to-the-cloud</category><content:encoded><![CDATA[<p>Every service in the platform had these six variables:</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>APP__GATEWAY__PRIVATE__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PRIVATE__PORT<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PRIVATE__SCHEME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http&#34;</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PUBLIC__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PUBLIC__PORT<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>APP__GATEWAY__PUBLIC__SCHEME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http&#34;</span>
</span></span></code></pre></div><p>Thirteen services, six variables each, one value. Reading any service&rsquo;s configuration, the architecture looked flat. Everything talked to the same host. That was the whole picture.</p>
<p>It wasn&rsquo;t.</p>
<h2 id="how-the-gateway-worked">How the gateway worked</h2>
<p>The gateway sat in front of every service and handled all inter-service traffic. A service calling the content API would construct a request to <code>http://platform.internal/content/api/</code> — the gateway received it, identified the target from the URL path, and forwarded it to the right backend. Every inter-service HTTP client in <code>framework.yaml</code> followed the same pattern:</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:#f92672">content.client</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">base_uri</span>: <span style="color:#e6db74">&#34;%http_client.gateway.base_uri%/content/api/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">headers</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">Host</span>: <span style="color:#e6db74">&#34;%env(APP__GATEWAY__PRIVATE__HOST)%&#34;</span>
</span></span></code></pre></div><p>The <code>http_client.gateway.base_uri</code> parameter was assembled from the GATEWAY vars. The gateway knew where each service ran. The services didn&rsquo;t need to know. From their perspective, everything was <code>platform.internal</code>.</p>
<p>This worked. For years, it worked well. Adding a service meant adding one DNS alias in the gateway config, not touching thirteen <code>.env</code> files. The gateway abstracted the topology. The services stayed decoupled from the infrastructure detail of who ran where.</p>
<h2 id="what-the-gateway-was-absorbing">What the gateway was absorbing</h2>
<p>The abstraction had a cost that didn&rsquo;t show up until you tried to read the system.</p>
<p>Looking at <code>content</code>&rsquo;s env file, you saw six gateway variables and nothing else about inter-service communication. To find out that <code>content</code> called <code>conversion</code>, <code>shorty</code>, and <code>media</code>, you had to read <code>framework.yaml</code>. To find out that <code>pilot</code> called ten external services, you had to trace through the HTTP clients one by one and count.</p>
<p>The number was ten. Authentication, bam, config, content, conversion, media, product, shorty, sitemap, social. Ten of the platform&rsquo;s thirteen services that <code>pilot</code> depended on at runtime, none of them visible from its configuration. Six variables said: talk to the gateway. They said nothing about the shape of what lay behind it.</p>
<p>That information existed — in the code, in the framework config, in the heads of the people who had built those integrations. It just didn&rsquo;t live anywhere you could read at a glance.</p>
<h2 id="what-kubernetes-made-explicit">What Kubernetes made explicit</h2>
<p>On-premise, the gateway was a single resolvable hostname. One DNS record, one set of variables, one place to update. Kubernetes doesn&rsquo;t work that way. Each service gets its own DNS name inside the cluster — <code>content.namespace.svc.cluster.local</code>, <code>conversion.namespace.svc.cluster.local</code>. Inter-service traffic goes directly, service to service, not through a shared gateway.</p>
<p>Moving to Kubernetes meant the gateway abstraction had to give way. Each service needed to know, concretely, where each of its dependencies lived. The six generic variables couldn&rsquo;t express that.</p>
<p>The refactor replaced them with per-target HOST variables — one per service dependency, named for the target:</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><span style="color:#75715e"># content/.env — content calls these four services</span>
</span></span><span style="display:flex;"><span>APP__CONFIG__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONVERSION__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__MEDIA__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SHORTY__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># pilot/.env — ten service dependencies</span>
</span></span><span style="display:flex;"><span>APP__AUTHENTICATION__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__BAM__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONFIG__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONTENT__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__CONVERSION__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__MEDIA__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__PRODUCT__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SHORTY__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SITEMAP__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span><span style="display:flex;"><span>APP__SOCIAL__HOST<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;platform.internal&#34;</span>
</span></span></code></pre></div><p>Each HTTP client in <code>framework.yaml</code> got its own <code>base_uri</code> built from its target&rsquo;s HOST variable, and the <code>Host</code> header gave way to a <code>User-Agent</code> that identified the caller:</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:#f92672">content.client</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">base_uri</span>: <span style="color:#e6db74">&#34;%env(APP__HTTP__SCHEME)%://%env(APP__CONTENT__HOST)%:%env(APP__HTTP__PORT)%/content/api/&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">headers</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">User-Agent</span>: <span style="color:#e6db74">&#34;Platform Content - %semver%&#34;</span>
</span></span></code></pre></div><p>The change isn&rsquo;t cosmetic. In the old setup, the explicit <code>Host</code> header ensured requests reached the correct gateway virtual host regardless of URL resolution. In the new setup, each client points directly at its target&rsquo;s DNS name — the right <code>Host</code> is derived from the <code>base_uri</code> automatically. The header slot doesn&rsquo;t go empty: <code>User-Agent</code> now identifies the calling service, which surfaces in logs and distributed traces without any additional instrumentation.</p>
<h2 id="the-discomfort-of-legibility">The discomfort of legibility</h2>
<p><code>pilot</code>&rsquo;s env file went from nine gateway variables to ten service-specific HOST variables. The file got longer. The architecture didn&rsquo;t get simpler — the ten dependencies were there before and they&rsquo;re still there now. What changed is that they&rsquo;re readable.</p>
<p><a href="https://12factor.net/config" target="_blank" rel="noopener noreferrer">Factor III</a>
 says to store configuration in the environment. The old approach satisfied that literally: six variables, all in env files, none hardcoded. But variables that collapse the entire dependency graph into a single opaque hostname aren&rsquo;t really configuration — they&rsquo;re a shorthand that trades legibility for convenience. Factor III doesn&rsquo;t ask only that configuration be externalized — it implicitly assumes the externalized configuration remains informative.</p>
<p>The refactor didn&rsquo;t simplify anything. It made the complexity visible. <code>pilot</code>&rsquo;s ten HOST variables document, in the <code>.env</code> file itself, the ten services it depends on. A new team member reading that file learns something real about the architecture. The old file taught them that there was a gateway.</p>
<p>There&rsquo;s a version of this story where you read the final state and conclude the team did unnecessary work — they replaced six variables with ten, all pointing at the same host anyway. In local development, <code>platform.internal</code> still resolves to the same place. The functional behavior didn&rsquo;t change.</p>
<p>The change is in what the configuration communicates. In Kubernetes, the HOST values diverge: each target gets its own cluster-internal DNS name, different per environment. The variables now carry real information. The refactor prepared the config to be honest about a topology it had been quietly simplifying for years.</p>
]]></content:encoded></item><item><title>Symfony 4.4 LTS: HttpClient, Mailer, Messenger, and the features that stayed</title><link>https://guillaumedelre.github.io/2020/01/04/symfony-4.4-lts-httpclient-mailer-messenger-and-the-features-that-stayed/</link><pubDate>Sat, 04 Jan 2020 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2020/01/04/symfony-4.4-lts-httpclient-mailer-messenger-and-the-features-that-stayed/</guid><description>Part 4 of 11 in &amp;quot;Symfony Releases&amp;quot;: Symfony 4.4 LTS ships a mature HttpClient and production-ready Messenger — the HTTP and async layers Symfony was missing.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 4.4 and 5.0 both landed November 21, 2019. 4.4 is the LTS: same feature set as 5.0, deprecation layer baked in, and a long support window for teams that can&rsquo;t follow every release.</p>
<p>The feature worth singling out arrived in 4.2 and matured through 4.3 and 4.4: <code>HttpClient</code>.</p>
<h2 id="httpclient">HttpClient</h2>
<p>PHP&rsquo;s built-in HTTP options (<code>file_get_contents</code> with stream contexts, cURL, Guzzle) each have their own model, their own quirks, and their own abstraction cost. Symfony 4.2 introduced <code>HttpClient</code>, a first-party HTTP client with one API over multiple transports.</p>
<p>The interface is clean:</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/users&#39;</span>);
</span></span><span style="display:flex;"><span>$users <span style="color:#f92672">=</span> $response<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">toArray</span>();
</span></span></code></pre></div><p>The implementation is async by default. Responses are lazy: the network request doesn&rsquo;t happen until you actually read the response. Multiple requests can be initiated and resolved as data arrives, no threads or callbacks needed.</p>
<p>The built-in mock transport (<code>MockHttpClient</code>) makes testing HTTP calls painless without spinning up servers or patching global functions.</p>
<h2 id="mailer">Mailer</h2>
<p>Also stabilized in 4.4: the <code>Mailer</code> component, replacing <code>SwiftMailerBundle</code> as the recommended email solution. Transport is configured via DSN:</p>
<pre tabindex="0"><code>MAILER_DSN=smtp://user:pass@smtp.example.com:587
</code></pre><p>The DSN approach means switching providers (Mailgun, Postmark, SES, local SMTP) is a config change, not a code change. Email testing uses a spooler by default in non-production environments.</p>
<h2 id="messenger-matures">Messenger matures</h2>
<p>The Messenger component landed in 3.4 as experimental. By 4.4 it&rsquo;s stable and battle-tested: async message handling with retry logic, failure transport, and adapters for AMQP, Redis, Doctrine, and in-process transports.</p>
<p>The pattern it enables (handle a request synchronously, dispatch work asynchronously, retry on failure) replaces a class of Gearman/RabbitMQ setups that required separate libraries and significant configuration.</p>
<h2 id="the-lts-window">The LTS window</h2>
<p>4.4 is supported for bugs until November 2022 and security fixes until November 2023. If you&rsquo;re on 4.x and want stability, this is a comfortable place to land. The deprecation warnings it introduces point directly at what 5.0 will require.</p>
<h2 id="the-messenger-component-from-experimental-to-production">The Messenger component, from experimental to production</h2>
<p>Messenger arrived in 4.1 as an experiment. The concept was simple: dispatch a message object to a bus, handle it immediately or route it to a transport for async processing. By 4.3 and 4.4, the experiment had become infrastructure.</p>
<p>The 4.3 release added a dedicated failure transport. When a message fails after all retry attempts, it goes somewhere recoverable rather than just disappearing:</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:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">messenger</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">failure_transport</span>: <span style="color:#ae81ff">failed</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">transports</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">async</span>: <span style="color:#e6db74">&#39;%env(MESSENGER_TRANSPORT_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">failed</span>: <span style="color:#e6db74">&#39;doctrine://default?queue_name=failed&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">App\Message\SendEmail</span>: <span style="color:#ae81ff">async</span>
</span></span></code></pre></div><p>Messages that land in <code>failed</code> can be inspected and retried manually. Before this, failed messages were a log entry and a headache. After this, they&rsquo;re a queue you can actually work with.</p>
<h2 id="event-dispatching-finally-using-objects-properly">Event dispatching, finally using objects properly</h2>
<p>Since the beginning, Symfony&rsquo;s event system used string event names as the primary identifier. You&rsquo;d define <code>OrderEvents::NEW_ORDER = 'order.new_order'</code>, listen on that string, and pass the event object as a secondary parameter.</p>
<p>4.3 flipped this around. The event object comes first, and the event name becomes optional:</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">// Before
</span></span></span><span style="display:flex;"><span>$dispatcher<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>(<span style="color:#a6e22e">OrderEvents</span><span style="color:#f92672">::</span><span style="color:#a6e22e">NEW_ORDER</span>, $event);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 4.3+
</span></span></span><span style="display:flex;"><span>$dispatcher<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>($event);
</span></span></code></pre></div><p>Omit the name and Symfony uses the class name as the identifier. Listeners and subscribers can now reference the class directly:</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">public</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getSubscribedEvents</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">array</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">OrderPlacedEvent</span><span style="color:#f92672">::</span><span style="color:#a6e22e">class</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;onOrderPlaced&#39;</span>,
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The HttpKernel events were renamed accordingly: <code>GetResponseEvent</code> became <code>RequestEvent</code>, <code>FilterResponseEvent</code> became <code>ResponseEvent</code>. The old names stayed as aliases through 4.x.</p>
<h2 id="vardumper-gets-a-server">VarDumper gets a server</h2>
<p><code>dump()</code> in a controller that returns JSON means your debug output gets injected straight into the response body. For API development, that&rsquo;s annoying enough to make people disable dumping entirely.</p>
<p>4.1 added a VarDumper server that captures dumps separately:</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>bin/console server:dump
</span></span></code></pre></div><p>Configure the dump destination in <code>config/packages/dev/debug.yaml</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">debug</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dump_destination</span>: <span style="color:#e6db74">&#34;tcp://%env(VAR_DUMPER_SERVER)%&#34;</span>
</span></span></code></pre></div><p>Now <code>dump()</code> in your API controller sends data to the server&rsquo;s console instead of polluting the response. The server shows the dump alongside its source file, the HTTP request that triggered it, and the timestamp.</p>
<h2 id="varexporter-for-when-var_export-fails-you">VarExporter, for when <code>var_export()</code> fails you</h2>
<p><code>var_export()</code> has two problems: it ignores serialization semantics and its output isn&rsquo;t PSR-2 compliant. The 4.2 VarExporter component fixes both.</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>$exported <span style="color:#f92672">=</span> <span style="color:#a6e22e">VarExporter</span><span style="color:#f92672">::</span><span style="color:#a6e22e">export</span>([<span style="color:#ae81ff">123</span>, [<span style="color:#e6db74">&#39;abc&#39;</span>, <span style="color:#66d9ef">true</span>]]);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Returns:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// [
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//     123,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//     [
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//         &#39;abc&#39;,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//         true,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">//     ],
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// ]
</span></span></span></code></pre></div><p>More importantly, it correctly handles objects implementing <code>Serializable</code>, <code>__sleep</code>, and <code>__wakeup</code>. Where <code>var_export()</code> silently drops serialization methods and exports raw properties, VarExporter produces code that calls the same hooks <code>unserialize()</code> would. The practical use case is cache warming: generating PHP files that can be loaded by OPcache without re-executing expensive computations.</p>
<h2 id="passwords-that-check-against-breach-databases">Passwords that check against breach databases</h2>
<p>The <code>NotCompromisedPassword</code> constraint arrived in 4.3. It checks submitted passwords against haveibeenpwned.com&rsquo;s breach database without sending the actual password anywhere.</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\Validator\Constraints</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">Assert</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">#[Assert\NotCompromisedPassword]
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">string</span> $plainPassword;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The implementation uses k-anonymity: SHA-1 hash the password, send only the first five characters to the API, get back all matching hashes, check locally. The password never leaves your server. For registration forms, adding this constraint is one line and a genuinely useful security signal.</p>
<h2 id="workflow-gets-context">Workflow gets context</h2>
<p>The Workflow component existed before 4.x, but 4.3 added context propagation: the ability to pass arbitrary data through a transition and access it in listeners.</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>$workflow<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">apply</span>($article, <span style="color:#e6db74">&#39;publish&#39;</span>, [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;user&#39;</span> <span style="color:#f92672">=&gt;</span> $user<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">getUsername</span>(),
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;reason&#39;</span> <span style="color:#f92672">=&gt;</span> $request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">request</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;reason&#39;</span>),
</span></span><span style="display:flex;"><span>]);
</span></span></code></pre></div><p>The context arrives in <code>TransitionEvent</code> and gets stored alongside the marking. For audit trails, this is the difference between knowing a transition happened and knowing who triggered it and why. You can also inject context from a subscriber without touching every <code>apply()</code> call, which is handy for cross-cutting concerns like timestamps or current user.</p>
<h2 id="the-autowiring-got-smarter">The autowiring got smarter</h2>
<p>4.2 added binding by type and name together. Before, you could bind by type (<code>LoggerInterface</code>) or by name (<code>$logger</code>), but not both at once. That caused problems when a service needs two different implementations of the same interface:</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:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">_defaults</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">bind</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">Psr\Log\LoggerInterface $orderLogger</span>: <span style="color:#e6db74">&#39;@monolog.logger.orders&#39;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">Psr\Log\LoggerInterface $paymentLogger</span>: <span style="color:#e6db74">&#39;@monolog.logger.payments&#39;</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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderService</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">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">LoggerInterface</span> $orderLogger,   <span style="color:#75715e">// gets monolog.logger.orders
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">LoggerInterface</span> $paymentLogger, <span style="color:#75715e">// gets monolog.logger.payments
</span></span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The match requires both type and argument name to align, so there&rsquo;s no risk of accidentally injecting the wrong logger.</p>
<h2 id="errorhandler-replaces-the-debug-component">ErrorHandler replaces the Debug component</h2>
<p>The <code>Debug</code> component, unchanged since 2013, had an awkward dependency on TwigBundle even for API-only apps. Any uncaught exception in a JSON API would render an HTML error page unless you wrote custom exception listeners.</p>
<p>4.4 extracts this into a dedicated <code>ErrorHandler</code> component. For non-HTML requests, error responses now follow RFC 7807 out of the box:</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>No Twig required. The format follows the <code>Accept</code> header: JSON for JSON requests, XML for XML requests. To customize further, you provide a normalizer via the Serializer component rather than a Twig template.</p>
<h2 id="php-74-preloading-wired-in-automatically">PHP 7.4 preloading, wired in automatically</h2>
<p>PHP 7.4 introduced OPcache preloading: load files into shared memory before any requests arrive, so they&rsquo;re available as compiled opcodes from the very first request. The practical gain is 30-50% faster response times with no code changes.</p>
<p>The catch is configuration: you need to specify exactly which files to preload in <code>php.ini</code>. Symfony 4.4 generates that file automatically in the cache directory:</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-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#75715e">; php.ini</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">opcache.preload</span><span style="color:#f92672">=</span><span style="color:#e6db74">/path/to/project/var/cache/prod/App_KernelProdContainer.preload.php</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">opcache.preload_user</span><span style="color:#f92672">=</span><span style="color:#e6db74">www-data</span>
</span></span></code></pre></div><p>Run <code>cache:warmup</code> in production and point OPcache at the generated file. Symfony preloads the container, compiled routes, and Twig templates: the files that are read on every request and never change between deploys.</p>
<h2 id="console-return-codes-and-no_color">Console: return codes and NO_COLOR</h2>
<p>Two small things in 4.4 that honestly should have existed earlier. Commands that don&rsquo;t return an integer from <code>execute()</code> now trigger a deprecation warning. In 5.0, the return type becomes mandatory. Returning <code>0</code> for success, non-zero for failure: standard Unix behavior, and it makes integration with process supervisors and CI pipelines unambiguous.</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">protected</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">execute</span>(<span style="color:#a6e22e">InputInterface</span> $input, <span style="color:#a6e22e">OutputInterface</span> $output)<span style="color:#f92672">:</span> <span style="color:#a6e22e">int</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Command</span><span style="color:#f92672">::</span><span style="color:#a6e22e">SUCCESS</span>; <span style="color:#75715e">// = 0
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The second: <code>NO_COLOR</code> environment variable support, following the convention from no-color.org. Set it and every Symfony console command drops ANSI escape codes regardless of what the terminal claims to support. Useful for CI environments that capture output as text and then choke on color codes embedded in logs.</p>
]]></content:encoded></item></channel></rss>