← Voltar ao blog

Dependency injection e virtual types no Magento 2: guia prático

Rodrigo Biazzi

Dependency injection no Magento 2 é, na maior parte, configuração. O lado PHP é injeção simples por construtor, mas o comportamento que você obtém depende dos arquivos di.xml espalhados por todos os módulos habilitados, mesclados por area e compilados em generated/metadata em produção. Depois que você entende como esse merge funciona, os virtual types deixam de parecer mágica e viram uma das ferramentas mais úteis do framework.

Tudo aqui vale para o Magento Open Source e o Adobe Commerce 2.4.8.

Injeção por construtor e o object manager

Uma classe declara no construtor o que precisa. O object manager lê a assinatura do construtor, resolve cada tipo e monta o objeto. Você nunca chama new para serviços e não deve chamar o object manager diretamente nas suas classes.

<?php
declare(strict_types=1);

namespace Acme\Feed\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Psr\Log\LoggerInterface;

class FeedBuilder
{
    public function __construct(
        private readonly ScopeConfigInterface $scopeConfig,
        private readonly LoggerInterface $logger,
        private readonly string $fileName = 'feed.xml',
        private readonly array $exporters = []
    ) {
    }
}

Interfaces são resolvidas por preferences. Parâmetros escalares e arrays recebem os valores default, a menos que o di.xml forneça outra coisa. Essa segunda parte é de onde vem a maior parte da flexibilidade.

Preferences

Uma preference mapeia uma interface (ou classe) para a implementação que o object manager deve criar:

<preference for="Acme\Feed\Api\FeedRepositoryInterface" type="Acme\Feed\Model\FeedRepository"/>

Uma preference para uma interface que é sua é normal. Uma preference que substitui uma classe do core é outra coisa. Só uma preference pode vencer para um dado tipo, e quem leva é o último módulo na ordem de carregamento. Duas extensões que preferem Magento\Checkout\Model\Session vão quebrar uma à outra em silêncio. Para mudar o comportamento de classes existentes, plugins quase sempre são a melhor opção. A comparação entre plugins, observers e preferences entra em detalhes nessa escolha.

Argumentos de type

O nó <type> permite definir os argumentos do construtor de uma classe sem mexer no código dela:

<type name="Acme\Feed\Model\FeedBuilder">
    <arguments>
        <argument name="fileName" xsi:type="string">google_feed.xml</argument>
        <argument name="exporters" xsi:type="array">
            <item name="price" xsi:type="object">Acme\Feed\Model\Exporter\Price</item>
            <item name="stock" xsi:type="object">Acme\Feed\Model\Exporter\Stock</item>
        </argument>
    </arguments>
</type>

O nome do argumento precisa bater exatamente com o nome do parâmetro PHP. Se você renomear um parâmetro do construtor, todo di.xml que o define para de funcionar sem erro, e o valor default passa a ser usado. Vale lembrar disso durante refatorações.

Os valores comuns de xsi:type são:

  • string, boolean, number e null para escalares.
  • object para o nome de uma classe ou virtual type. O object manager cria ou compartilha o objeto.
  • array com nós <item> aninhados.
  • const para usar uma constante de classe, por exemplo Magento\Store\Model\ScopeInterface::SCOPE_STORE.
  • init_parameter para ler um valor passado à aplicação no bootstrap.

Arrays são mesclados entre módulos pelo nome do item. Se o seu módulo adiciona um item chamado stock ao mesmo argumento, ele substitui o item stock existente. Um nome novo adiciona uma entrada nova. É assim que os pools do core são estendidos, como a lista de comandos do CLI descrita no guia de comando CLI customizado, ou os modifier pools dos UI components do formulário de produto.

Virtual types

Um virtual type é uma configuração nomeada de uma classe existente com argumentos de construtor diferentes. Não existe arquivo PHP. O object manager cria uma instância da classe original e injeta os argumentos que você definiu para o nome virtual.

O exemplo clássico é um arquivo de log dedicado. Em vez de escrever uma classe de handler e uma de logger, você configura as duas:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="Acme\Feed\Logger\Handler" type="Magento\Framework\Logger\Handler\Base">
        <arguments>
            <argument name="fileName" xsi:type="string">/var/log/acme_feed.log</argument>
        </arguments>
    </virtualType>

    <virtualType name="Acme\Feed\Logger\Logger" type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="name" xsi:type="string">acmeFeed</argument>
            <argument name="handlers" xsi:type="array">
                <item name="file" xsi:type="object">Acme\Feed\Logger\Handler</item>
            </argument>
        </arguments>
    </virtualType>

    <type name="Acme\Feed\Model\FeedBuilder">
        <arguments>
            <argument name="logger" xsi:type="object">Acme\Feed\Logger\Logger</argument>
        </arguments>
    </type>
</config>

O FeedBuilder continua com type hint em Psr\Log\LoggerInterface. Ele recebe um logger do Monolog que escreve em var/log/acme_feed.log e mais nada muda. O artigo sobre logger, logger customizado com Monolog, cobre a parte de logging com mais profundidade.

Nomear virtual types com um namespace que parece uma classe é convenção, não exigência. Ajuda porque o nome aparece em stack traces e na saída do dev:di:info, e deixa claro qual módulo é o dono.

O que virtual types não podem fazer

  • Você não pode usar um virtual type como type hint no PHP. Ele não existe como classe. Você o injeta por um argumento do di.xml.
  • Você não pode adicionar métodos. Se precisa de comportamento diferente e não só de configuração diferente, escreva uma classe de verdade.
  • Plugins declarados na classe original também se aplicam ao virtual type, porque a instância é dessa classe. Você não pode declarar um plugin no nome do virtual type em si. A documentação de plugins lista virtual types como um dos casos que plugins não suportam.

Virtual types que estendem virtual types

O atributo type de um virtual type pode apontar para outro virtual type. Os argumentos são herdados e podem ser sobrescritos. O core usa muito isso em collections e data providers. É poderoso, mas uma cadeia de três ou quatro virtual types é difícil de acompanhar. Se você se pega lendo vários arquivos para saber o que uma classe recebe, uma classe pequena de verdade pode ficar mais clara.

Escopo por area

A configuração em etc/di.xml é global. Os arquivos em etc/frontend/di.xml, etc/adminhtml/di.xml, etc/webapi_rest/di.xml, etc/webapi_soap/di.xml, etc/graphql/di.xml e etc/crontab/di.xml são mesclados por cima da configuração global só quando aquela area é carregada.

Use isso para manter plugins do frontend fora do admin e da API, e o contrário também. Um plugin que só importa na loja e é declarado globalmente roda também em cron, consumers e comandos CLI, o que custa tempo e pode causar efeitos colaterais surpreendentes quando não há sessão nem contexto de store.

Instâncias shared e não shared

Por padrão o object manager compartilha instâncias. Toda classe que injeta o FeedBuilder recebe o mesmo objeto durante uma requisição. Para serviços que não guardam estado da requisição, é isso que você quer.

Se uma classe guarda estado que não pode vazar entre usuários, defina shared="false":

<type name="Acme\Feed\Model\Exporter\Batch" shared="false"/>

Um tipo não shared é criado novo para cada classe que o injeta, mas cada consumidor ainda mantém a própria instância durante toda a requisição. Se você precisa de um objeto novo a cada chamada de um método, injete uma factory.

Estado compartilhado importa mais do que antes no 2.4.8 por causa do trabalho feito para suportar application server com GraphQL. Classes que guardam dados por requisição em propriedades devem resetá-los. O core usa o Magento\Framework\ObjectManager\ResetAfterRequestInterface para isso. Se você escreve um módulo que roda em GraphQL, evite guardar dados da requisição em propriedades de serviços shared.

Debug da configuração de DI

Quando uma classe não recebe o que você espera, confira a configuração mesclada em vez de adivinhar:

bin/magento dev:di:info "Acme\Feed\Model\FeedBuilder"

A saída mostra a preference, os parâmetros do construtor com os tipos e valores resolvidos e os plugins ligados à classe. É a forma mais rápida de descobrir que o nome de um argumento não bate ou que outro módulo sobrescreve a sua preference.

Outras coisas para conferir:

  • Em developer mode, uma classe velha em generated/code pode sobreviver a uma mudança de construtor. Remova a pasta afetada e recarregue.
  • Depois de qualquer mudança no di.xml em production mode, rode bin/magento setup:di:compile. O metadata compilado em generated/metadata é o que a produção lê.
  • Confira a area. Uma preference em etc/frontend/di.xml é invisível para um comando CLI.
  • Confira a ordem de carregamento dos módulos com <sequence> no module.xml se dois módulos configuram o mesmo argumento e o errado ganha.

Diretrizes práticas

  • Injete interfaces, não classes concretas, sempre que existir uma interface.
  • Mantenha os construtores sem lógica. Não carregue dados nem chame serviços no construtor, porque o objeto pode ser criado e nunca usado.
  • Use virtual types para diferenças de configuração e classes de verdade para diferenças de comportamento.
  • Use proxies para dependências pesadas que só são necessárias em alguns caminhos do código. O artigo sobre factories e proxies mostra como.
  • Nunca use ObjectManager::getInstance() em código novo. Ele esconde dependências e atrapalha os testes. O core ainda o tem em alguns lugares por retrocompatibilidade, o que não é motivo para copiar.

FAQ

Qual a diferença entre um virtual type e uma preference?

Uma preference diz ao Magento qual classe usar quando um tipo é solicitado. Um virtual type cria um nome novo para uma classe existente com argumentos de construtor diferentes. Preferences substituem, virtual types configuram.

Por que os argumentos do meu di.xml são ignorados?

Em geral o nome do argumento não bate com o nome do parâmetro do construtor, o arquivo está na pasta de area errada ou o generated code está velho. Rode bin/magento dev:di:info na classe para ver o que o Magento realmente resolveu.

Posso usar um virtual type como type hint no construtor?

Não. Virtual types só existem na configuração. Use como type hint a classe original ou a interface dela e injete o virtual type por um argumento do di.xml.

Preciso rodar setup:di:compile depois de editar o di.xml?

Em production mode, sim. Em developer mode, limpar o config cache normalmente basta, além de remover as classes geradas se os construtores mudaram.

Atendo como freelancer em Magento e Adobe Commerce. Se o seu projeto tem conflitos de di.xml entre extensões ou você precisa de outro par de olhos na arquitetura de módulos, fale comigo.

Artigos relacionados