Como criar atributos de extensão no Magento 2
Tags: php, magento2, tutorial, braziliandevs
Published: 2022-03-17 17:05:02

## Contextualizando

### O que é um atributo de extensão?

Os atributos no Magento 2 são propriedades das entidades que auxiliam na variedade de opções. Os módulos personalizados não podem alterar as interfaces das entidades nativas, entretanto, a maioria das interfaces das entidades possuem um funcionalidade chamada **atributos de extensão**. Para saber se a interface da entidade possui atributos de extensão, basta observar se a mesma estende da interface `\Magento\Framework\Api\ExtensibleDataInterface` *e* possui os métodos `getExtensionAttributes()` e `setExtensionAttributes()`.

---

## Código para criar um atributo

### Criando o atributo de extensão

#### extension_attributes.xml

Para criar os atributos de extensão é necessário criar um arquivo chamado `extension_attributes.xml` que deve seguir a estrutura de pasta `\{Vendor}\{Module}\etc\extension_attributes.xml`

```xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="{Vendor}\{Module}\Api\Data\{EntityName}Interface">
        <attribute code="{attr_name_object}" type="{Vendor}\{Module}\{Directory}\{Class}" />
        <attribute code="{attr_name_string}" type="string" />
        <attribute code="{attr_name_boolean}" type="boolean" />
        <attribute code="{attr_name_integer}" type="int" />
        <attribute code="{attr_name_float}" type="float" />
        <attribute code="{attr_name_array}" type="{type}[]" />
        <attribute code="{attr_name_array}" type="{Vendor}\{Module}\Api\Data\{EntityName}Interface[]">
            <resources>
                <resource ref="{Vendor}_{Module}::{acl_path}" />
            </resources>
            <join reference_table="{table_name}" reference_field="{column_name}" join_on_field="{interface_field}">
                <field>{field_name}</field>
            </join>
        </attribute>
    </extension_attributes>
</config>
```

O nó `<extension_attributes>` só possui um atributo, o `for`, que indicará qual a entidade que receberá os novos atributos de extensão e que será implementada a interface `\Magento\Framework\Api\ExtensibleDataInterface` e possuir os métodos `getExtensionAttributes()` e `setExtensionAttributes()`.

O nó `<attribute>` é filho do nó `<extension_attributes>` e é obrigatório, este possui o atributo `code`, que será o nome do atributo (deverá ser escrito em snake case) e o atributo `type`, que indicará o tipo do atributo.

O nó `<resources>` é filho do nó `<attribute>` é opcional e não possui atributos, apenas pode ter o nó filho `<resource>`, já este possui o atributo `ref`, que referencia a seção a um recurso ACL para fornecer permissões das configurações.

O nó `<join>` é filho do nó `<attribute>` é opcional e possui o atributo `reference_table`, que faz uma operação de *join* com a tabela referenciada neste atributo, com o valor da coluna referenciada no atributo `reference_field` e `join_on_field` é a coluna da tabela associada a interface.

O nó `<field>` é filho do nó `<join>` e é obrigatório quando este for inserido. Este nó não possui atributos e nem outros nós filhos, apenas valor, que especifica a propriedade da tabela referenciada que irá unir a interface definida.

#### db_schema.xml

É necessário adicionar uma coluna na tabela desejada para o campo do atributo de extensão criado no arquivo `extension_attributes.xml`. 
Utilizarei o arquivo de `db_schema.xml` para adicionar a coluna na tabela, mas pode ser feito como *UpgradeSchema* ou *SchemaPatch*.

```xml
<?xml version="1.0" ?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    ...
    <table name="{table_name)">
        <column xsi:type="{column_type}" name="{column_name)" comment="{Column Comment}"/>
    </table>
</schema>
```

Para saber mais sobre como criar e/ou atualizar campos na tabela via `db_schema.xml` basta acessar o post "[Como criar tabelas personalizadas no banco de dados com db_schema.xml no Magento 2](https://dev.to/santanaluc94/como-criar-tabelas-personalizadas-no-banco-de-dados-com-dbschema-xml-no-magento-2-3jlh)".
Para saber mais sobre como criar e/ou atualizar campos na tabela via `SchemaPatch` basta acessar o post "[Como criar/atualizar tabelas e dados com DataPatch e SchemaPatch no Magento 2](https://dev.to/santanaluc94/como-criaratualizar-tabelas-e-dados-com-datapatch-e-schemapatch-3aag)".
Para saber mais sobre como criar e/ou atualizar campos na tabela na forma antiga que o Magento utiliza, `UpgradeSchema`, basta acessar o post "[Como criar/atualizar tabelas com InstallSchema e UpgradeShema no Magento 2](https://dev.to/santanaluc94/como-criaratualizar-tabelas-com-installschema-e-upgradeshema-264p)".

### Acessando os atributos de extensão

Assim que criar o arquivo `extension_attributes.xml` já fica disponível para acessar e definir valores dos atributos via métodos mágicos, apenas adicionando o prefixo `get` e `set` nos nomes definidos dos atributos.

```php
$extAttributes = $entityName->getExtensionAttributes();
$extAttributes->getAttrNameObject();
$extAttributes->getAttrNameString();
$extAttributes->getAttrNameBoolean();
$extAttributes->getAttrNameInteger();
$extAttributes->getAttrNameFloat();
$extAttributes->getAttrNameArray();

$extAttributes->setAttrNameObject($objectValue);
$extAttributes->setAttrNameString($stringValue);
$extAttributes->setAttrNameBoolean($booleanValue);
$extAttributes->setAttrNameInteger($integerValue);
$extAttributes->setAttrNameFloat($floatValue);
$extAttributes->setAttrNameArray($arrayValue);
```

### Atributo de extensão na entidade

#### Interface

No Magento, primeiro criamos a interface do repositório no caminho `\{Vendor}\{Module}\Api\Data`. Caso a interface que queira adicionar os atributos de extensão não pertença ao código fonte do Magento 2, basta herdar a interface `\Magento\Framework\Api\ExtensibleDataInterface`.
Para saber mais sobre como sobreescrever arquivos através no post "[Como sobrescrever arquivos no Magento 2 através de preferences](https://dev.to/santanaluc94/como-sobrescrever-arquivos-no-magento-2-atraves-de-preferences-384g)".

```php
<?php

namespace {Vendor}\{Module}\Api\Data;

use Magento\Framework\Api\ExtensibleDataInterface;

interface {EntityName}Interface extends ExtensibleDataInterface
{
    public const {ATTR_NAME} = '{attribute_name}';

    public function get{MethodName}(): {type};

    public function set{MethodName}(): self;

    public function getExtensionAttributes(): \{Vendor}\{Module}\Api\Data\{EntityName}ExtensionInterface;

    public function setExtensionAttributes(\{Vendor}\{Module}\Api\Data\{EntityName}ExtensionInterface $extensionAttributes): self;
}
```

#### di.xml

Após a criação da interface do repositório é necessário a sobrescrição através do arquivo `di.xml`, para que o arquivo fique devidamente implementado e possa descrever em como os métodos serão executados.

```xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    ...
    <preference for="{Vendor}\{Module}\Api\Data\{EntityName}Interface" type="{Vendor}\{Module}\Model\Data\{EntityName}"/>
    ...
</config>
```

#### Model

Criado a interface do repositório e a sobrescrição da interface através do arquivo `di.xml`, falta apenas implementar os métodos da interface na classe.

```php
<?php

namespace {Vendor}\{Module}\Model\Data;

use Magento\Framework\Model\AbstractExtensibleModel;
use {Vendor}\{Module}\Api\Data\{EntityName}Interface;

class {EntityName} extends AbstractExtensibleModel implements {EntityName}Interface
{
    public function get{MethodName}(): {type}
    {
        return $this->_get({EntityName}Interface::{ATTR_NAME});
    }

    public function set{MethodName}(string $attrName): {EntityName}Interface
    {
        return $this->setData({EntityName}Interface::ATTR_NAME, $attrName);
    }

    public function getExtensionAttributes(): \{Vendor}\{Module}\Api\Data\{EntityName}ExtensionInterface
    {
        return $this->_getExtensionAttributes();
    }

    public function setExtensionAttributes(\{Vendor}\{Module}\Api\Data\{EntityName}ExtensionInterface $extensionAttributes) {
        return $this->_setExtensionAttributes($extensionAttributes);
    }
}
```

### Plugin para salvar/recuperar os dados do atributo de extensão

#### di.xml

Para saber mais sobre plugin basta acessar o post "[Como criar plugins no Magento 2](https://dev.to/santanaluc94/como-criar-plugins-no-magento-2-h64)".


```xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    ...
    <type name="Magento\{Module}\Model\{Directory}\{EntityName}">
        <plugin name="{plugin_name}" type="{Vendor}\{Module}\Plugin\Model\{PluginEntityName}" disabled="false" sortOrder="10" />
    </type>
    <type name="Magento\{Module}\Model\{Directory}\{EntityName}Repository">
        <plugin name="{plugin_name}" type="{Vendor}\{Module}\Plugin\{Directory}\{PluginRepositoryName}" disabled="false" sortOrder="20" />
    </type>
    ...
</config>
```

#### Plugins

Será necessário criar o método `afterLoad()` para adicionar os atributos de extensão no retorno do objeto quando forem recuperado do banco via a classe da entidade.
Será necessário criar o método `beforeSave()` para adicionar valores no atributo de extensão antes de salvar o objeto no banco de dados através da entidade.

```php
<?php

namespace {Vendor}\{Module}\Plugin\{Directory};

use Magento\{Module}\Api\Data\{EntityName}ExtensionFactory;
use Magento\{Module}\Api\Data\{EntityName}Interface as Magento{EntityName}Interface;
use {Vendor}\{Module}\Api\Data\{EntityName}Interface;

class {PluginEntityName}
{
    public function __construct(
        private {EntityName}ExtensionFactory $extensionAttributeFactory
    ) {
    }

    public function afterLoad(Magento{EntityName}Interface $subject): Magento{EntityName}Interface {
        $extensionAttributes = $subject->getExtensionAttributes() ?: $this->extensionAttributeFactory->create();

        $extensionAttributes->set{MethodName}($subject->getData({EntityName}Interface::ATTR_NAME));

        $subject->setExtensionAttributes($extensionAttributes);

        return $subject;
    }

    public function beforeSave(Magento{EntityName}Interface $subject): array 
    {
        $extensionAttributes = $subject->getExtensionAttributes() ?: $this->extensionAttributeFactory->create();

        if ($extensionAttributes !== null && $extensionAttributes->set{MethodName}() !== null) {
            $subject->set{MethodName}($extensionAttributes->get{MethodName}());
        }

        return [$subject];
    }
}
```

Será necessário criar os métodos `afterGetById()` e `afterGetList()` para adicionar os atributos de extensão no retorno do(s) objeto(s) quando forem recuperado do banco via o repositório.
Será necessário criar o método `beforeSave()` para adicionar valores no atributo de extensão antes de salvar o objeto no banco de dados através do repositório.

```php
<?php

namespace {Vendor}\{Module}\Plugin\{Directory};

use Magento\{Module}\Api\Data\{EntityName}ExtensionFactory;
use Magento\{Module}\Api\{EntityName}RepositoryInterface;
use {Vendor}\{Module}\Api\Data\{EntityName}Interface;
use Magento\{Module}\Api\Data\{EntityName}Interface as Magento{EntityName}Interface;
use Magento\{Module}\Api\Data\{EntityName}SearchResultsInterface;

class {PluginRepositoryName}
{
    public function __construct(
        private {EntityName}ExtensionFactory $extensionAttributeFactory
    ) {
    }

    public function afterGetById(
        {EntityName}RepositoryInterface $subject,
        Magento{EntityName}Interface $entity
    ): Magento{EntityName}Interface {
        $extensionAttributes = $entity->getExtensionAttributes() ?: $this->extensionAttributeFactory->create();

        $extensionAttributes->set{MethodName}($entity->getData({EntityName}Interface::ATTR_NAME));

        $entity->setExtensionAttributes($extensionAttributes);

        return $entity;
    }

    public function afterGetList(
        {EntityName}RepositoryInterface $subject,
        {EntityName}SearchResultsInterface $resultCart
    ) {
        /** @var Magento{EntityName}Interface $entity */
        foreach ($resultCart->getItems() as $entity) {
            $this->afterGetById($subject, $entity);
        }

        return $resultCart;
    }

    public function beforeSave(
        {EntityName}RepositoryInterface $subject,
        Magento{EntityName}Interface $entity
    ): array {
        $extensionAttributes = $entity->getExtensionAttributes() ?: $this->extensionAttributeFactory->create();

        if ($extensionAttributes !== null && $extensionAttributes->set{MethodName}() !== null) {
            $entity->set{MethodName}($extensionAttributes->get{MethodName}());
        }

        return [$entity];
    }
}
```

---

## Finalizando

> Valores entre chaves (`{test}`) devem ser alterados na implementação do código.

### Habilitando as alterações

Apague os arquivos que são gerados na compilação do Magento e execute o comando PHP para gerar a configuração das injeções de dependência e todas as classes ausentes que precisam ser geradas (*proxys*, *interceptors*, etc) e para limpar todos os caches de armazenamento em cache do processos.

```bash
rm -rf var/generation/
rm -rf generated/
php bin/magento setup:di:compile
php bin/magento cache:clean
php bin/magento flush
```

### Diretórios e Arquivos

Segue a a lista de diretórios e arquivos que devem ser criados.

```
- app/
  - code/
    - {Vendor}/
        - {Module}/
          - etc/
            - module.xml
            - extension_attributes.xml
          - registration.php
          - composer.json
```