← Voltar ao blog

Factories, proxies e a pasta generated no Magento 2

Rodrigo Biazzi

O Magento 2 escreve muito PHP por você. Factories, proxies, interceptors e classes de extension attributes não estão em nenhum módulo. Eles são gerados em generated/code na primeira vez que são necessários, ou antecipadamente pelo setup:di:compile. A maioria dos erros que as pessoas atribuem a cache são na verdade problemas dessas classes geradas, então compensa saber o que cada uma faz.

Isto vale para o Magento Open Source e o Adobe Commerce 2.4.8 em PHP 8.3 ou 8.4.

Por que o framework gera código

O object manager compartilha instâncias por padrão. Isso é certo para serviços, mas errado para entidades. Se você injetar Magento\Catalog\Model\Product diretamente, recebe um único objeto de produto compartilhado, e toda classe que o injeta vê os mesmos dados. Você precisa de um jeito de pedir uma instância nova sem chamar new e sem usar o object manager na sua classe.

O mesmo vale para lazy loading. Algumas dependências são caras de construir, e você não quer criá-las em um construtor se o método que as usa raramente é chamado. E os plugins precisam de um lugar para se encaixar. O Magento resolve os três com classes geradas, seguindo convenções de nome que o autoloader reconhece.

Factories

Uma factory é uma classe com um método create() que devolve uma instância nova de um dado tipo. Você nunca a escreve. Você usa como type hint um nome de classe terminado em Factory e o Magento a gera:

<?php
declare(strict_types=1);

namespace Acme\Notice\Model;

use Acme\Notice\Model\ResourceModel\Notice\CollectionFactory;

class NoticeCreator
{
    public function __construct(
        private readonly NoticeFactory $noticeFactory,
        private readonly CollectionFactory $collectionFactory
    ) {
    }

    public function build(string $message): Notice
    {
        $notice = $this->noticeFactory->create();
        $notice->setMessage($message);
        return $notice;
    }

    public function countActive(): int
    {
        return $this->collectionFactory->create()
            ->addFieldToFilter('is_active', 1)
            ->getSize();
    }
}

Note que o NoticeFactory é usado sem uma instrução use porque está no mesmo namespace do Notice. O arquivo gerado vai para generated/code/Acme/Notice/Model/NoticeFactory.php.

O create() aceita um array de argumentos do construtor por nome. Para data models construídos sobre AbstractModel ou DataObject, a forma usual é passar os dados iniciais:

$notice = $this->noticeFactory->create(['data' => ['message' => 'Free shipping this week']]);

Também dá para gerar factories para interfaces. O Magento\Catalog\Api\Data\ProductInterfaceFactory cria a classe para a qual a preference de ProductInterface aponta. É a escolha certa quando seu código trabalha com o contrato e não deve se importar com a implementação.

Quando não usar uma factory

Serviços não precisam de factories. Se uma classe não tem estado por instância, injete diretamente. Uma factory para um repository, um helper ou um leitor de configuração só adiciona uma chamada create() que devolve um objeto novo de que você não precisava. Em alguns casos até quebra coisas, porque duas instâncias de um serviço que guarda dados em cache internamente não vão compartilhar esse cache.

Proxies

Um proxy é uma subclasse da classe original que não guarda nada até você chamar um método nela. A primeira chamada cria o objeto real pelo object manager e repassa a chamada. Todas as chamadas seguintes vão direto para essa instância.

Você não usa um proxy como type hint no PHP. Mantém o tipo original no construtor e troca no di.xml:

<type name="Acme\Notice\Console\Command\ImportNotices">
    <arguments>
        <argument name="importer" xsi:type="object">Acme\Notice\Model\Importer\Proxy</argument>
    </arguments>
</type>

A classe gerada é generated/code/Acme/Notice/Model/Importer/Proxy.php. Sua classe não sabe que recebeu um proxy, e esse é o ponto. A decisão sobre lazy loading é configuração, não código.

Onde proxies importam

  • Comandos CLI. Rodar o bin/magento constrói todos os comandos registrados para listá-los. Se um comando injeta algo que abre uma conexão com o banco ou lê uma configuração grande na cadeia do construtor, toda chamada do bin/magento paga por isso, e alguns comandos quebram antes de o banco existir, por exemplo durante o setup:install. Proxies nas dependências pesadas de um comando evitam isso. O artigo sobre comando CLI customizado mostra um exemplo completo.
  • Sessões. O core injeta Magento\Customer\Model\Session\Proxy e Magento\Checkout\Model\Session\Proxy em muitos lugares. Iniciar uma sessão tem efeitos colaterais, incluindo headers e cookies, então só deve acontecer quando a sessão é realmente usada.
  • Plugins e observers com caminhos raros. Se um plugin só usa uma dependência em um branch, um proxy mantém o caminho comum barato.

Proxies não podem ser gerados para classes final, porque o proxy precisa estender a classe original para continuar compatível com o type hint dela.

Interceptors

Quando qualquer plugin é declarado para uma classe, o Magento gera Vendor\Module\Class\Interceptor, uma subclasse que sobrescreve cada método público e roda os plugins before, around e after. O object manager devolve o interceptor no lugar da classe original sempre que existem plugins para ela.

Isso tem consequências práticas:

  • Plugins só funcionam em métodos públicos de classes criadas pelo object manager. Uma classe instanciada com new não recebe interceptor.
  • Classes final e métodos final não podem ser interceptados.
  • Os stack traces mostram a classe Interceptor e o ___callPlugins(). Isso é normal e indica que há plugins envolvidos.

As decisões de design em torno de plugins são cobertas em plugins vs observers vs preferences.

Outras classes geradas

Para entidades com extension attributes, o Magento gera ProductExtensionInterface, ProductExtension e uma factory correspondente a partir dos arquivos extension_attributes.xml mesclados. É por isso que adicionar um extension attribute novo exige uma etapa de regeneração antes de o getter existir. O artigo sobre extension attributes cobre isso em detalhes.

A pasta generated/metadata guarda a configuração compilada do object manager em production mode. Não são classes PHP que você leria, mas uma cópia velha causa o mesmo tipo de erro confuso.

Developer mode vs production mode

Em developer mode, uma classe gerada ausente é criada na hora pelo autoloader na primeira vez que é solicitada. Isso é conveniente, mas também significa que arquivos gerados antigos continuam por perto quando você muda um construtor. Uma factory ou interceptor gerado para a assinatura antiga pode causar erros que parecem não ter relação com a sua mudança.

Em production mode, você gera tudo antecipadamente:

bin/magento setup:di:compile

Isso varre todos os módulos, gera factories, proxies, interceptors e classes de extension, e grava a configuração compilada. Em um pipeline de build decente isso roda uma vez no CI, e o resultado é enviado aos servidores como parte do artefato do release.

Resolvendo problemas com código gerado

Estes erros quase sempre vêm de código gerado:

  • Class "Acme\Notice\Model\NoticeFactory" does not exist em production mode. A compilação não rodou depois que a classe foi adicionada, ou falhou no meio.
  • Argument #2 must be of type ... logo depois de uma mudança de construtor. Um interceptor velho ainda está passando os argumentos antigos.
  • Plugins que não disparam. O interceptor foi gerado antes de o plugin ser declarado e nunca foi reconstruído.

Em developer mode a solução costuma ser remover a pasta afetada e deixar regenerar:

rm -rf generated/code/Acme/Notice
bin/magento cache:clean config

Remover todo o generated/code e o generated/metadata também é seguro. Só custa uma primeira requisição mais lenta. Em production mode, reconstrua com setup:di:compile e limpe o cache. Nunca edite um arquivo gerado na mão, porque ele será sobrescrito.

Não faça commit da pasta generated no controle de versão. Ela pertence à saída do build, assim como o pub/static. O artigo sobre deploy sem downtime mostra onde a compilação se encaixa em um pipeline de release.

Um checklist curto

  1. Use uma factory quando precisar de uma instância nova de entidade ou de uma collection nova.
  2. Injete serviços diretamente, sem factories.
  3. Configure um proxy no di.xml para dependências pesadas que nem sempre são usadas, e para toda dependência pesada de um comando CLI.
  4. Não torne classes ou métodos públicos final se outro código pode precisar de plugin neles.
  5. Depois de mudanças em construtor ou plugin, limpe o código gerado relevante em desenvolvimento e recompile em produção.

FAQ

Devo escrever minhas próprias classes factory?

Quase nunca. O Magento as gera a partir do nome da classe. Escreva uma manualmente só quando precisar de lógica de criação customizada, e então dê um nome que não termine em Factory para evitar confusão com as geradas.

Posso usar uma classe Proxy como type hint no meu construtor?

Funciona, mas não é o padrão pretendido. Use o type hint da classe original e troque para o proxy no di.xml, para que o lazy loading continue sendo uma decisão de configuração.

É seguro apagar a pasta generated?

Sim, em developer mode o Magento reconstrói o que precisa na próxima requisição. Em production mode você precisa rodar o setup:di:compile de novo antes de a loja funcionar.

Por que vejo Interceptor nos meus stack traces?

Porque pelo menos um plugin está declarado para aquela classe. O interceptor é uma subclasse gerada que roda os plugins em volta do método original.

Atendo como freelancer em Magento e Adobe Commerce. Se você está brigando com erros de código gerado em um pipeline de deploy ou precisa de ajuda para refatorar um módulo, fale comigo.

Artigos relacionados