<?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>Architecture on Guillaume Delré</title><link>https://guillaumedelre.github.io/tags/architecture/</link><description>Recent content in Architecture on Guillaume Delré</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 26 Jan 2022 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/tags/architecture/index.xml" rel="self" type="application/rss+xml"/><item><title>Swarrot vs Symfony Messenger: a real-world comparison</title><link>https://guillaumedelre.github.io/2022/01/26/swarrot-vs-symfony-messenger-a-real-world-comparison/</link><pubDate>Wed, 26 Jan 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2022/01/26/swarrot-vs-symfony-messenger-a-real-world-comparison/</guid><description>Swarrot and Symfony Messenger both handle RabbitMQ in PHP. Here is why we kept Swarrot after seriously evaluating a migration.</description><content:encoded><![CDATA[<p>We migrated a media microservices platform to Symfony 6 at the start of 2022. Twelve services, most of them consuming messages from RabbitMQ via <a href="https://github.com/swarrot/swarrot" target="_blank" rel="noopener noreferrer">Swarrot</a>. Symfony 6 made <a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Messenger</a> more central than ever, and during the migration planning a developer asked the obvious question: why not switch at the same time?</p>
<p>It ships with the framework. It has retry logic, native AMQP support, first-party documentation. Our setup looked artisanal by comparison.</p>
<p>Fair question. We took it seriously. Here&rsquo;s what we found.</p>
<h2 id="wiring-the-topology-by-hand">Wiring the topology by hand</h2>
<p>Swarrot is a consumer library that wraps the PECL AMQP extension. It reads bytes from a queue, runs them through a chain of processors (their term for middleware), and lets your code decide what to do with the payload. That&rsquo;s really it.</p>
<p>The middleware chain is the interesting part. Processors are nested decorators, each wrapping the next. The outer layers handle infrastructure concerns before the message even reaches your business logic:</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">middleware_stack</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.signal_handler&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.max_execution_time&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.exception_catcher&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.doctrine_object_manager&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.ack&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;app.processor.retry&#39;</span>
</span></span></code></pre></div><p><code>signal_handler</code> sits at the top because it needs to catch <code>SIGTERM</code> before any other processor sees it. <code>ack</code> sits near the bottom because you only acknowledge the message after processing succeeds. The order is not arbitrary, and it&rsquo;s entirely visible in configuration.</p>
<p>The topology is equally explicit. You declare everything yourself: exchanges, routing keys, retry queues, dead-letter queues:</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">messages_types</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_retry</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.retry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_dead</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.dead</span>
</span></span></code></pre></div><p>Three entries per logical message type: main queue, retry queue, dead-letter queue. Everything that exists on the broker is named right here. The config is verbose but honest: no inference, no convention over configuration. If a queue exists in RabbitMQ, you can trace it to a single line of YAML.</p>
<h2 id="when-the-class-name-becomes-the-route">When the class name becomes the route</h2>
<p><a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Symfony Messenger</a> operates one level higher. You define a message class, a handler, and a transport. The library handles serialization, routing, retry, and failure queues automatically.</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">class</span> <span style="color:#a6e22e">IngestContent</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">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $contentId,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $source,
</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:#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">transports</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">async</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">dsn</span>: <span style="color:#e6db74">&#39;%env(MESSENGER_TRANSPORT_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">retry_strategy</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">max_retries</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">delay</span>: <span style="color:#ae81ff">1000</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">&#39;App\Message\IngestContent&#39;</span>: <span style="color:#ae81ff">async</span>
</span></span></code></pre></div><p>Messenger serializes the object, puts it on the transport, and deserializes it on the other end into the correct class. No manual topology, no explicit exchange names. The class name is the routing primitive.</p>
<p>That last sentence is exactly where things got complicated for us.</p>
<h2 id="where-typing-becomes-coupling">Where typing becomes coupling</h2>
<p>Messenger assumes that the producer and the consumer share a PHP class definition. That&rsquo;s fine for a single app, or for services that share a dedicated contracts package. In a monorepo of independent Symfony applications, it creates coupling that simply doesn&rsquo;t exist today.</p>
<p>Take a content ingestion message that twelve services consume. With Swarrot, each service reads the raw JSON payload and picks the fields it cares about. Adding a new field means updating the producer. Consumers that don&rsquo;t need the field keep working without any modification.</p>
<p>With Messenger, <code>IngestContent</code> must be defined somewhere that all twelve services can reference. That means either:</p>
<ul>
<li>A shared PHP package, versioned, deployed, and maintained across services. Every schema change becomes a cross-service coordination exercise.</li>
<li>Duplicated classes in each service, which drift silently apart under pressure.</li>
</ul>
<p>Neither is free. The shared package approach inverts the ownership model: the message schema becomes a dependency rather than a contract defined at the boundary. The duplication approach is just the original problem deferred.</p>
<p>The root difference is what a message represents. Messenger is designed for <strong>typed commands</strong>: an object that carries meaning and dispatches to a specific handler. Swarrot treats messages as <strong>opaque data</strong>: bytes that flow through a topology, processed by whatever consumer happens to be listening. If your messages are data, the extra abstraction Messenger adds doesn&rsquo;t help you. It creates friction.</p>
<h2 id="the-blocker">The blocker</h2>
<p>The serialization problem was the decisive one. In a monorepo where services are autonomous, sharing PHP classes between them isn&rsquo;t architecturally neutral: it&rsquo;s a coupling decision that makes future changes harder. We would have been trading a nominally &ldquo;legacy&rdquo; library for a more modern one while introducing exactly the kind of tight coupling we&rsquo;d spent years avoiding.</p>
<p>There were secondary concerns too. The PECL AMQP extension gives direct access to broker features (message priorities, per-queue TTL, headers exchange routing) that Messenger abstracts away. And migrating fifteen consumers without a flag day means running both libraries in parallel, which is a real operational constraint.</p>
<p>But the serialization issue alone would have been enough.</p>
<h2 id="data-or-commands-thats-the-question">Data or commands: that&rsquo;s the question</h2>
<p>The choice isn&rsquo;t about library quality. Messenger is well-maintained, well-documented, and integrates cleanly into the Symfony ecosystem.</p>
<p>The question to ask first is: what are your messages?</p>
<p>If they are typed commands with a known schema and a single authoritative consumer, Messenger is a natural fit. You write a class, a handler, configure a transport, and the infrastructure handles the rest.</p>
<p>If they are data payloads consumed by multiple independent services, each of which owns its own deserialization, the abstraction Messenger adds works against you. Swarrot&rsquo;s explicit topology and raw payload model give you more control where you actually need it.</p>
<p>One real limitation to keep in mind: Swarrot is tied to the PECL AMQP extension, which only implements AMQP 0-9-1. That means RabbitMQ (or a compatible broker) is a hard dependency. If your infrastructure ever moves toward an AMQP 1.0 broker (Azure Service Bus, ActiveMQ Artemis), Swarrot can&rsquo;t follow. Messenger&rsquo;s transport layer abstracts this cleanly: changing brokers means changing a DSN, not rewriting consumers.</p>
<p>If broker portability is a requirement, or likely to become one, that changes the calculus significantly.</p>
<p>Swarrot isn&rsquo;t legacy to migrate away from. For now, it&rsquo;s the right fit: AMQP routing as the primitive, messages as data, RabbitMQ as a long-term infrastructure choice.</p>
<p>That could change. A shared contracts package, a new broker requirement, a greenfield service that doesn&rsquo;t carry the existing topology weight: any of these could tip the balance toward Messenger. The library isn&rsquo;t wrong for this platform. It may just be the right answer for a future version of it.</p>
]]></content:encoded></item><item><title>Symfony 4.0: Flex and the end of the Standard Edition</title><link>https://guillaumedelre.github.io/2018/01/14/symfony-4.0-flex-and-the-end-of-the-standard-edition/</link><pubDate>Sun, 14 Jan 2018 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2018/01/14/symfony-4.0-flex-and-the-end-of-the-standard-edition/</guid><description>Part 3 of 11 in &amp;quot;Symfony Releases&amp;quot;: Symfony 4.0 killed the Standard Edition and introduced Flex: a microframework that grows only as far as you actually need.</description><category>symfony-releases</category><content:encoded><![CDATA[<p>Symfony 4.0 released November 30, 2017, same day as 3.4. The shared release date is pretty much the only thing they have in common.</p>
<p>4.0 is a different philosophy. The Symfony Standard Edition, the monolithic starting point that bundled everything and left you to remove what you didn&rsquo;t need, is gone. In its place: a microframework that grows.</p>
<h2 id="flex">Flex</h2>
<p>Symfony Flex is a Composer plugin that changes how you install Symfony packages. Before Flex, adding a bundle meant: install via Composer, register in <code>AppKernel.php</code>, add config to <code>config/</code>, update routing if needed. Four steps, all manual.</p>
<p>With Flex, installing a package runs a &ldquo;recipe&rdquo;: a set of automated steps that registers the bundle, generates a config skeleton, and wires routing. Installing Doctrine:</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>composer require symfony/orm-pack
</span></span></code></pre></div><p>That command installs the packages, creates <code>config/packages/doctrine.yaml</code>, adds the env variable stubs to <code>.env</code>, and registers everything. One command, zero manual steps.</p>
<p>Recipes are community-contributed and hosted on a central server. Quality varies, but for major packages they&rsquo;re maintained alongside the packages themselves.</p>
<h2 id="the-new-project-structure">The new project structure</h2>
<p>The Standard Edition layout (<code>app/</code>, <code>src/</code>, <code>web/</code>) is replaced by a leaner structure. Config lives in <code>config/</code> split by environment. The public directory is now <code>public/</code>, not <code>web/</code>. The kernel is smaller. Controllers are plain classes, no <code>extends Controller</code> required.</p>
<p>More importantly, the default <code>services.yaml</code> uses the 3.3 autowiring defaults that make explicit service configuration mostly unnecessary. New projects start minimal and grow by adding what they actually need.</p>
<h2 id="services-private-by-default">Services private by default</h2>
<p>4.0&rsquo;s biggest BC break for existing apps: all services are private by default. You can&rsquo;t fetch a service from the container directly anymore, it has to be injected. This is the right call from a DI perspective, but it breaks anything that used <code>$this-&gt;get('service_id')</code> in controllers.</p>
<p>The migration path is <code>AbstractController</code>, which provides the same convenience methods through lazy service locators rather than raw container access.</p>
<h2 id="what-was-removed">What was removed</h2>
<p>4.0 is clean because it removes everything deprecated in 3.4:</p>
<ul>
<li>The old form events, the old security interfaces, the old configuration formats</li>
<li>Support for PHP &lt; 7.1.3</li>
<li>The ClassLoader component</li>
<li>ACL support from SecurityBundle</li>
</ul>
<p>The removals are aggressive. Apps that skipped fixing their 3.4 deprecations will have a rough time. Apps that did the cleanup beforehand have a smooth path.</p>
<p>Symfony 4.0 is the reset the framework needed. The Standard Edition had accumulated years of &ldquo;this is how it&rsquo;s done&rdquo; that Flex sweeps away in one shot.</p>
<h2 id="environment-variables-that-actually-know-their-type">Environment variables that actually know their type</h2>
<p>Before 3.4 and 4.0, environment variables were strings. Always. Trying to inject <code>DATABASE_PORT</code> into an <code>int</code> parameter would silently break or blow up with a type error. The fix was ugly: cast in PHP or avoid typed parameters entirely.</p>
<p>4.0 ships with env var processors that handle the conversion at the container level:</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">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">app.connection.port</span>: <span style="color:#e6db74">&#39;%env(int:DATABASE_PORT)%&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">app.debug_mode</span>: <span style="color:#e6db74">&#39;%env(bool:APP_DEBUG)%&#39;</span>
</span></span></code></pre></div><p>Beyond casting, processors can decode base64, load from files, parse JSON, or resolve container parameters within a value. The <code>json:file:</code> combination turned into a clean pattern for loading secrets from mounted files in containerized deployments:</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">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env(SECRETS_FILE)</span>: <span style="color:#e6db74">&#39;/run/secrets/app.json&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">app.secrets</span>: <span style="color:#e6db74">&#39;%env(json:file:SECRETS_FILE)%&#39;</span>
</span></span></code></pre></div><p>You can also write custom processors by implementing <code>EnvVarProcessorInterface</code> and tagging the service. Looks like overkill until the day you need it.</p>
<h2 id="tagged-services-without-the-boilerplate">Tagged services without the boilerplate</h2>
<p>Before 4.0, collecting all services with a given tag into one service meant writing a compiler pass. Forty lines of PHP to say &ldquo;give me everything tagged <code>app.handler</code>.&rdquo;</p>
<p>3.4 introduced the <code>!tagged</code> YAML shorthand, and 4.0 carries it forward:</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">App\HandlerCollection</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">arguments</span>: [!<span style="color:#ae81ff">tagged app.handler]</span>
</span></span></code></pre></div><p>The collection is lazy by default when type-hinted as <code>iterable</code>, so services aren&rsquo;t instantiated until you actually iterate. This replaced a whole category of compiler passes that existed for the sole purpose of building lists.</p>
<h2 id="php-as-a-configuration-format">PHP as a configuration format</h2>
<p>YAML has been the default for so long it feels required. It isn&rsquo;t. 4.0 ships with PHP-based configuration using a fluent 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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#75715e">// config/services.php
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> <span style="color:#66d9ef">function</span> (<span style="color:#a6e22e">ContainerConfigurator</span> $container) {
</span></span><span style="display:flex;"><span>    $services <span style="color:#f92672">=</span> $container<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">services</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">defaults</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">autowire</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">autoconfigure</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $services<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">load</span>(<span style="color:#e6db74">&#39;App\\&#39;</span>, <span style="color:#e6db74">&#39;../src/&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">exclude</span>(<span style="color:#e6db74">&#39;../src/{Entity,Repository}&#39;</span>);
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Same approach works for routes. The practical benefit: IDE autocompletion, type checking, and actual PHP logic in configuration without the <code>%</code> parameter interpolation syntax. YAML isn&rsquo;t going anywhere, but now you have a choice.</p>
<h2 id="argon2i-because-bcrypt-was-already-aging">Argon2i, because bcrypt was already aging</h2>
<p>Symfony 3.4/4.0 added Argon2i support, winner of the 2015 Password Hashing Competition. Configuration is one line:</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">security</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">encoders</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">App\Entity\User</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">algorithm</span>: <span style="color:#ae81ff">argon2i</span>
</span></span></code></pre></div><p>Argon2i is built into PHP 7.2+ and available via the sodium extension on earlier versions. Like bcrypt, it&rsquo;s self-salting, no need to manage salt columns. Unlike bcrypt, it&rsquo;s designed to resist GPU-based attacks with configurable memory usage. If you&rsquo;re starting a new project on 4.0, there&rsquo;s really no reason to reach for bcrypt.</p>
<h2 id="the-form-layer-gets-a-bootstrap-4-theme">The form layer gets a Bootstrap 4 theme</h2>
<p>The existing Bootstrap 3 form theme has been around since Symfony 2.x. Bootstrap 4 ships as a first-class option in 4.0:</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">twig</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">form_themes</span>: [<span style="color:#e6db74">&#39;bootstrap_4_layout.html.twig&#39;</span>]
</span></span></code></pre></div><p>More useful in practice: the <code>tel</code> and <code>color</code> HTML5 input types are now available as <code>TelType</code> and <code>ColorType</code> form types. Before, you had to write custom types or override raw widgets for those.</p>
<h2 id="local-service-binding">Local service binding</h2>
<p>Global <code>_defaults</code> bindings apply to all services. Sometimes you need a binding scoped to a specific class or namespace, like different logger instances for different subsystems.</p>
<p>4.0 supports per-service <code>bind</code> for exactly that:</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">App\Service\OrderService</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</span>: <span style="color:#e6db74">&#39;@monolog.logger.orders&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">App\Service\PaymentService</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</span>: <span style="color:#e6db74">&#39;@monolog.logger.payments&#39;</span>
</span></span></code></pre></div><p>Same interface, two different implementations, no factory, no extra configuration. Small feature, but it kills a whole category of awkward workarounds.</p>
]]></content:encoded></item></channel></rss>