← Voltar ao blog

Como criar um tipo de produto customizado no Magento 2

Rodrigo Biazzi

O Magento 2 vem com produtos simple, virtual, configurable, grouped, bundle e downloadable, e o Adobe Commerce adiciona gift cards. De vez em quando um projeto parece precisar de um tipo novo: um plano de serviço, um aluguel, uma caixa de assinatura, um item sob encomenda. Às vezes essa é a decisão certa. Com mais frequência, um produto simple ou virtual com alguns atributos e um pouco de lógica customizada resolveria com muito menos risco. Este artigo cobre os dois lados, para Magento Open Source e Adobe Commerce 2.4.8.

Você realmente precisa de um tipo novo?

Um tipo de produto muda como o Magento trata um produto em todo lugar: formulários do admin, carrinho, quote items, indexação de preço, estoque, busca, documentos de vendas e toda extensão que verifica o code do tipo. Antes de criar um, veja se estas opções mais simples atendem ao requisito:

  • Um produto simple ou virtual com um atributo, como is_service_plan, e plugins que mudam o comportamento quando ele está definido. Isso mantém todo o suporte do core e de terceiros intacto.
  • Custom options para pequenas entradas por pedido, como um texto de gravação.
  • Produtos bundle ou grouped para conjuntos de itens.
  • Produtos downloadable para qualquer coisa entregue como arquivo.

Um tipo customizado faz sentido quando o comportamento do produto é diferente em vários lugares ao mesmo tempo, por exemplo um cálculo de preço diferente, um fluxo de adicionar ao carrinho diferente e um atendimento diferente, e quando você precisa que o tipo seja visível para as integrações como algo distinto. Se só uma dessas coisas é verdade, um atributo mais um plugin costuma ser o melhor design.

Declarando o tipo

Um tipo é declarado em etc/product_types.xml. Este exemplo cria um tipo service_plan que se comporta como um produto virtual com o seu próprio price model:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Catalog:etc/product_types.xsd">
    <type name="service_plan"
          label="Service Plan"
          modelInstance="Acme\ServicePlan\Model\Product\Type\ServicePlan"
          indexPriority="80"
          sortOrder="80"
          isQty="true">
        <priceModel instance="Acme\ServicePlan\Model\Product\Type\ServicePlan\Price"/>
        <customAttributes>
            <attribute name="refundable" value="true"/>
            <attribute name="is_real_product" value="true"/>
        </customAttributes>
    </type>
</config>
  • name é o code do tipo guardado em catalog_product_entity.type_id. Escolha com cuidado, porque mudá-lo depois significa uma migração de dados.
  • label é o que os admins veem no menu Add Product.
  • isQty decide se o tipo tem quantidade em estoque. Com o MSI, também decide se os source items podem ser gerenciados para o tipo.
  • customAttributes são flags lidas por outras partes do sistema. A abordagem mais simples é abrir vendor/magento/module-catalog/etc/product_types.xml, achar o tipo do core mais próximo do seu e copiar as flags dele.

O mesmo arquivo pode declarar um indexer de preço com um nó indexerModel, e uma seção composableTypes controla quais tipos podem ser usados como filhos de produtos bundle e grouped. Veja como o core declara simple e virtual no mesmo arquivo e siga esse padrão em vez de inventar o seu próprio indexer.

O type model

O type model é onde mora o comportamento específico do tipo. Estender o tipo do core mais próximo poupa muito trabalho:

<?php
declare(strict_types=1);

namespace Acme\ServicePlan\Model\Product\Type;

use Magento\Catalog\Model\Product\Type\Virtual;

class ServicePlan extends Virtual
{
    public const TYPE_CODE = 'service_plan';
}

Essa classe vazia já funciona. Ela herda o isVirtual(), então quotes só com planos de serviço pulam a etapa de frete, e herda a preparação do add to cart do AbstractType.

Os métodos que você mais provavelmente vai sobrescrever:

  • _prepareProduct() para validar ou transformar o buy request quando o produto é adicionado ao carrinho. Retorne uma string para mostrar um erro, ou um array de produtos em caso de sucesso.
  • getOrderOptions() para guardar informação extra no order item, para que apareça no admin e nos e-mails.
  • isSalable() se a disponibilidade depende de algo além de estoque e status.
  • hasWeight(), para o qual o Virtual já retorna false.

Mantenha o type model enxuto. Coloque a lógica de negócio em services e chame-os a partir do type model, para que possa ser testado sem montar objetos de produto completos.

O price model

<?php
declare(strict_types=1);

namespace Acme\ServicePlan\Model\Product\Type\ServicePlan;

use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Type\Price as BasePrice;

class Price extends BasePrice
{
    /**
     * @param float|null $qty
     * @param Product $product
     * @return float
     */
    public function getFinalPrice($qty, $product)
    {
        $finalPrice = (float) parent::getFinalPrice($qty, $product);
        return max(0.0, $finalPrice);
    }
}

Saiba onde esse preço é usado. O price model é chamado quando um produto é carregado e quando os quote items são totalizados. Páginas de categoria, resultados de busca e ordenação por preço leem do índice de preço, que é construído por SQL no indexer e não chama o seu price model em PHP. Se o seu tipo calcula o preço de forma diferente, o preço na listagem e o preço no carrinho vão divergir, a menos que o indexer faça o mesmo cálculo. Essa é a parte de um tipo customizado que mais dá errado.

Fazendo os atributos do core valerem para o novo tipo

Atributos do core como price e tax_class_id têm uma lista apply_to de tipos de produto. Um tipo novo não está nessa lista, então o formulário do admin não vai mostrar um campo de preço. Corrija com um data patch:

<?php
declare(strict_types=1);

namespace Acme\ServicePlan\Setup\Patch\Data;

use Acme\ServicePlan\Model\Product\Type\ServicePlan;
use Magento\Catalog\Model\Product;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;

class ApplyPriceAttributesToServicePlan implements DataPatchInterface
{
    private const ATTRIBUTES = [
        'price',
        'special_price',
        'special_from_date',
        'special_to_date',
        'cost',
        'tier_price',
        'tax_class_id',
    ];

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

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

        foreach (self::ATTRIBUTES as $code) {
            $applyTo = (string) $eavSetup->getAttribute(Product::ENTITY, $code, 'apply_to');
            if ($applyTo === '') {
                continue;
            }
            $types = explode(',', $applyTo);
            if (!in_array(ServicePlan::TYPE_CODE, $types, true)) {
                $types[] = ServicePlan::TYPE_CODE;
                $eavSetup->updateAttribute(Product::ENTITY, $code, 'apply_to', implode(',', $types));
            }
        }

        return $this;
    }

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

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

Um apply_to vazio significa que o atributo já vale para todos os tipos, e é por isso que esses são pulados. Atributos dos seus próprios módulos devem seguir as mesmas regras, como descrito em o artigo sobre data patch de atributo de produto.

Frontend e admin

Na página de produto, o Magento adiciona um layout handle por tipo: catalog_product_view_type_service_plan. Crie um arquivo de layout com esse nome para adicionar ou remover blocks só para o seu tipo, sem tocar nos outros produtos.

No admin, o novo tipo aparece no split button Add Product automaticamente. Campos ou painéis específicos do tipo no formulário de produto são adicionados por form modifiers registrados no virtual type Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool, que é o mesmo mecanismo que o core usa para os painéis de configurable e bundle. Verifique o tipo do produto dentro do modifier e retorne o meta sem alteração para os outros tipos.

Inventário, indexação e integrações

  • Inventário. Com isQty="true", o MSI permite atribuir sources e quantidades ao tipo. Teste salable quantity, reservations e indexação de estoque com o seu tipo antes do lançamento. O artigo sobre MSI cobre como essas peças se encaixam.
  • Indexação de preço. Rode um reindex completo e confira catalog_product_index_price para os seus produtos. Se estiverem faltando, eles não vão aparecer nas listagens de categoria com preços nem nos filtros de preço.
  • GraphQL. A query products resolve cada item para um tipo GraphQL concreto com base no tipo do produto. Um tipo novo precisa do seu próprio tipo GraphQL implementando ProductInterface e de um type resolver registrado na configuração de GraphQL do catálogo, caso contrário as queries que o retornam falham. O padrão é parecido com o de o artigo sobre resolver GraphQL customizado.
  • Extensões de terceiros. Feeds, conectores de ERP, módulos de busca e extensões de checkout muitas vezes têm listas de codes de tipo conhecidos. Faça uma auditoria das instaladas no projeto atrás de checagens de tipo fixas no código.

FAQ

Devo estender AbstractType ou um type model existente?

Estenda o tipo existente mais próximo, normalmente Simple ou Virtual. O AbstractType obriga você a implementar e testar muito mais, e raramente você precisa dessa liberdade.

Por que meu tipo customizado não mostra um campo de preço no admin?

Os atributos de preço têm uma lista apply_to que não inclui o code do seu tipo. Adicione com um data patch, ou deixe apply_to vazio se o atributo deve valer para todos os tipos.

Por que o preço na página de categoria é diferente do preço no carrinho?

As listagens leem do índice de preço, enquanto o carrinho usa o price model. Se o seu tipo muda o cálculo de preço, o price indexer precisa aplicar as mesmas regras.

Posso mudar o tipo de um produto existente depois?

Não com segurança pelo admin. Exige atualizar o type_id e limpar os dados específicos do tipo, depois reindexar. Escolha o tipo e o code dele antes de importar produtos.

Atendo como freelancer em Magento e Adobe Commerce. Se você está decidindo entre um tipo de produto customizado e um design mais simples, fale comigo e posso ajudar a pesar as opções.

Artigos relacionados