← Voltar ao blog

Criar um atributo de produto no Magento 2 com data patch

Rodrigo Biazzi

Atributos de produto podem ser criados no admin em um minuto, então por que escrever código para eles? Porque um atributo criado à mão em produção não existe no staging, na máquina do desenvolvedor nem no banco do seu CI. Se o seu módulo lê esse atributo, ele precisa criá-lo. No Magento 2.3 em diante, o jeito de fazer isso é um data patch. Este guia mostra como escrever um para Magento Open Source e Adobe Commerce 2.4.8, com as opções que importam e os erros que mais aparecem.

Como os data patches funcionam

Um data patch é uma classe em Setup/Patch/Data que implementa Magento\Framework\Setup\Patch\DataPatchInterface. Durante o bin/magento setup:upgrade, o Magento executa todo patch cujo nome de classe ainda não está na tabela patch_list e depois o registra ali. Um patch roda uma vez por banco de dados. Não há número de versão para incrementar, como nos antigos scripts InstallData e UpgradeData.

Um patch declara outros patches de que depende com getDependencies() e nomes antigos que ele substitui com getAliases(). Para o quadro geral de schema e setup de dados, veja o artigo sobre declarative schema e data patches.

Um atributo de texto

<?php
declare(strict_types=1);

namespace Acme\Catalog\Setup\Patch\Data;

use Magento\Catalog\Model\Product;
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\Patch\PatchRevertableInterface;

class AddManufacturerPartNumberAttribute implements DataPatchInterface, PatchRevertableInterface
{
    private const ATTRIBUTE_CODE = 'manufacturer_part_number';

    public function __construct(
        private readonly ModuleDataSetupInterface $moduleDataSetup,
        private readonly EavSetupFactory $eavSetupFactory
    ) {
    }

    public function apply(): self
    {
        $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);

        $eavSetup->addAttribute(Product::ENTITY, self::ATTRIBUTE_CODE, [
            'type' => 'varchar',
            'label' => 'Manufacturer Part Number',
            'input' => 'text',
            'required' => false,
            'global' => ScopedAttributeInterface::SCOPE_GLOBAL,
            'group' => 'General',
            'sort_order' => 50,
            'visible' => true,
            'user_defined' => true,
            'searchable' => true,
            'filterable' => false,
            'comparable' => false,
            'visible_on_front' => true,
            'used_in_product_listing' => false,
            'is_used_in_grid' => true,
            'is_visible_in_grid' => false,
            'is_filterable_in_grid' => true,
            'unique' => false,
        ]);

        return $this;
    }

    public function revert(): void
    {
        $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
        $eavSetup->removeAttribute(Product::ENTITY, self::ATTRIBUTE_CODE);
    }

    public static function getDependencies(): array
    {
        return [];
    }

    public function getAliases(): array
    {
        return [];
    }
}

Rode bin/magento setup:upgrade e o atributo aparece no grupo General de todos os attribute sets de produto.

Um detalhe a saber antes do primeiro deploy: se já existe um atributo com o mesmo code, o addAttribute() o atualiza com as configurações do seu patch em vez de falhar. Isso é conveniente quando um atributo foi criado primeiro à mão em produção e agora você quer ele em código. Também significa que o seu patch vai sobrescrever o que um admin tiver alterado. Compare as configurações de produção com o patch antes de publicar.

As opções que importam

O array passado ao addAttribute() usa chaves curtas que o catalog setup mapeia para colunas de eav_attribute e catalog_eav_attribute. As que vale pensar a cada vez:

  • type define a tabela de backend: varchar, int, decimal, text, datetime ou static. Atributos select guardam IDs de opções, então usam int. Multiselect guarda uma lista separada por vírgulas e usa varchar ou text.
  • input é o campo do admin: text, textarea, select, multiselect, boolean, date, price e mais alguns.
  • global é o scope. SCOPE_GLOBAL, SCOPE_WEBSITE ou SCOPE_STORE. Use global a menos que o valor realmente mude por store view. Atributos com scope de store multiplicam as linhas nas tabelas de valores e deixam a indexação mais lenta.
  • group coloca o atributo nesse grupo de todos os attribute sets. Deixe de fora se quiser adicionar o atributo só a sets específicos.
  • user_defined controla se os admins podem excluir o atributo. Defina como true para atributos customizados normais.
  • searchable, filterable e filterable_in_search afetam o índice de busca e a navegação em camadas. Só ative quando precisar, porque cada um adiciona dados ao OpenSearch e trabalho à reindexação.
  • used_in_product_listing adiciona o atributo aos dados carregados nas páginas de categoria. É fácil ligar e esquecer, e cada atributo habilitado deixa as queries de listagem mais pesadas.
  • apply_to limita o atributo a tipos de produto, como uma string separada por vírgulas, por exemplo simple,virtual. Vazio significa todos os tipos.

Mais sobre o lado de performance dessas flags em o artigo de boas práticas para atributos de produto.

Um atributo select com opções

Para um dropdown com uma lista fixa que os admins devem poder editar depois, deixe o Magento guardar as opções em eav_attribute_option:

$eavSetup->addAttribute(Product::ENTITY, 'material', [
    'type' => 'int',
    'label' => 'Material',
    'input' => 'select',
    'source' => \Magento\Eav\Model\Entity\Attribute\Source\Table::class,
    'required' => false,
    'global' => ScopedAttributeInterface::SCOPE_GLOBAL,
    'group' => 'General',
    'user_defined' => true,
    'filterable' => 1,
    'searchable' => true,
    'visible_on_front' => true,
    'option' => [
        'values' => ['Cotton', 'Linen', 'Wool', 'Polyester'],
    ],
]);

A chave option cria labels no nível do admin para cada valor. Labels por store view podem ser adicionados depois no admin ou em outro patch. Como as opções ficam guardadas como dados, os admins podem adicionar valores novos sem deploy, que costuma ser o que o negócio quer.

Um atributo select com source model

Quando as opções vêm do código ou de outro sistema, escreva um source model e aponte o source para ele:

<?php
declare(strict_types=1);

namespace Acme\Catalog\Model\Attribute\Source;

use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;

class WarrantyPeriod extends AbstractSource
{
    public function getAllOptions(): array
    {
        if ($this->_options === null) {
            $this->_options = [
                ['value' => '', 'label' => __('-- Please Select --')],
                ['value' => 12, 'label' => __('12 months')],
                ['value' => 24, 'label' => __('24 months')],
                ['value' => 36, 'label' => __('36 months')],
            ];
        }
        return $this->_options;
    }
}

Opções definidas assim não podem ser editadas no admin, e mudar um valor depois significa migrar os dados guardados. Use para listas que fazem parte da lógica de negócio, não para valores de marketing como cores ou materiais.

Se o módulo dono do source model for removido um dia, o atributo continua apontando para uma classe que não existe mais, e a página de edição do produto quebra. Remova o atributo ou troque o source dele antes de desinstalar, como explicado em o artigo sobre remover um módulo com segurança.

Adicionando a attribute sets específicos

Com user_defined true e sem group, o atributo é criado mas não é atribuído a nenhum attribute set. Para adicioná-lo a um set:

$entityTypeId = $eavSetup->getEntityTypeId(Product::ENTITY);
$attributeSetId = $eavSetup->getAttributeSetId($entityTypeId, 'Clothing');
$groupId = $eavSetup->getAttributeGroupId($entityTypeId, $attributeSetId, 'General');

$eavSetup->addAttributeToGroup($entityTypeId, $attributeSetId, $groupId, 'material', 60);

Os nomes dos attribute sets variam entre projetos. Se o set pode não existir, verifique antes e pule, para o patch não falhar em uma instalação nova.

Atualizando um atributo existente

Nunca edite um patch que já rodou. Ele não vai rodar de novo nos bancos que já o registraram, então os ambientes se afastam. Escreva um patch novo e use o updateAttribute():

$eavSetup->updateAttribute(Product::ENTITY, 'material', 'is_filterable_in_search', 1);
$eavSetup->updateAttribute(Product::ENTITY, 'material', 'used_in_product_listing', 0);

Declare o patch original em getDependencies() para a ordem ser garantida.

Erros comuns

  • Esquecer o source em um atributo select. O campo aparece sem opções, ou o frontend mostra IDs de opção em vez de labels.
  • Usar varchar para valores que serão filtrados ou ordenados como números. Escolha o tipo de backend pela forma como o dado é consultado.
  • Scope de store para tudo por padrão. Na prática, a maioria dos atributos é global.
  • Apagar uma linha de patch_list para rodar um patch de novo em produção. Isso funciona em um banco local para testes, mas em ambientes compartilhados esconde o problema real, que é que a mudança pertence a um patch novo.
  • Rodar um reindex e limpar o cache depois não é opcional se o atributo é searchable ou filterable. Rode bin/magento indexer:reindex catalogsearch_fulltext e os indexers de EAV, ou deixe o cron cuidar disso se os indexers estiverem em schedule.

FAQ

Como rodo um data patch de novo?

Em um banco local você pode apagar a linha dele em patch_list e rodar setup:upgrade. Para mudanças de verdade, escreva um patch novo, porque o original não vai rodar de novo nos ambientes que já o aplicaram.

Qual a diferença entre scope global, website e store?

Global guarda um valor para todas as stores. Os scopes de website e store permitem valores diferentes por website ou store view, ao custo de mais linhas e indexação mais lenta. Use o menor scope de que você realmente precisa.

O revert roda automaticamente quando eu desabilito o módulo?

Não. O revert roda quando o módulo é desinstalado com bin/magento module:uninstall, que só funciona para módulos instalados pelo Composer. Desabilitar um módulo não remove os atributos dele.

Por que meu atributo novo não aparece no frontend?

Confira visible_on_front, used_in_product_listing para páginas de categoria, a atribuição ao attribute set e se o template do tema realmente o renderiza. Depois reindexe e limpe o cache.

Atendo como freelancer em Magento e Adobe Commerce. Se você precisa de ajuda com a estrutura do catálogo ou com atributos EAV, fale comigo.

Artigos relacionados