Durante minha carreira como desenvolvedor Magento percebi que em muitos momentos, enquanto estamos desenvolvendo nossos módulos e adaptações para a plataforma, precisamos separar algumas configurações que vamos implementar em nossos módulos em arquivos diferentes.

Normalmente um desenvolvedor decide utilizar um novo arquivo de configuração quando ele não quer disponibilizar a possibilidade destas configurações serem alteradas via painel administrativo, porém quer manter a flexibilidade para qualquer outro módulo poder estender as configurações de seu módulo e, assim, modificar o comportamento do seu módulo, sem tocar em um único arquivo do módulo original, ou seja, é uma excelente prática, pois estamos adaptando nosso módulo para utilizar o conceito de Loose Coupling (fraco acoplamento, em tradução literal, mas como um amigo meu me disse, o termo fica horrível se traduzido, então prefiro me referenciar à ele em inglês), portanto nosso módulo será “Loosely Coupled”, ou seja, não terá forte acoplamento ou hard dependency com qualquer outro módulo do Magento.

Aí você me pergunta: “mas por que não posso utilizar os arquivos que o Magento já possui, como o config.xml, por exemplo, ao invés de criar um arquivo totalmente novo?”. Bom a resposta para essa pergunta vai depender da versão do Magento que você está utilizando.

No caso do Magento 1 as coisas são bem mais simples e, já que a maioria das configurações podem ser misturadas, trata-se simplesmente de uma questão de facilitar a visibilidade das suas configurações mesmo, e charme, claro. Mas ainda assim, é recomendável que você sempre separe novas configurações em arquivos específicos, caso queira mostrar mais facilmente à outros desenvolvedores que seu módulo possui um arquivo distinto e ele pode estender essas configurações facilmente sem tocar no seu módulo. Um exemplo clássico disso está no Magento Commerce (antigo Magento Enterprise) em sua versão 1 com o módulo de Full Page Cache (Enterprise_PageCache) que possui um arquivo específico (cache.xml) para fazer a leitura de todos os blocos que devem ser cacheados separadamente. Quando o Full Page Cache faz a leitura das configurações de cache pelo nome do arquivo o Magento faz um loop em todos os módulos para verificar quais módulos possuem o arquivo cache.xml em seu diretório etc e, os que forem encontrados, são compilados em tempo de execução para que o módulo Enterprise_PageCache os reconheça e faça o cache também dos blocos de outros módulos.

Veja como isso tudo se resume a uma pequena linha de código no Magento 1:

$config = Mage::getConfig()->loadModulesConfiguration('cache.xml');

Nesta linha de código o Magento 1 utiliza o object Mage_Core_Model_Config e solicita que ele faça o carregamento e a leitura de todos os arquivos com o nome cache.xml. Por sua vez o objeto Config do Magento 1 faz a iteração em todos os módulos carregados tentando encontrar o arquivo especificado. Isso acontece no método loadModulesConfiguration().

class Mage_Core_Model_Config extends Mage_Core_Model_Config_Base
{

    ...


    /**
     * Iterate all active modules "etc" folders and combine data from
     * specidied xml file name to one object
     *
     * @param   string $fileName
     * @param   null|Mage_Core_Model_Config_Base $mergeToObject
     * @return  Mage_Core_Model_Config_Base
     */
    public function loadModulesConfiguration($fileName, $mergeToObject = null, $mergeModel=null)
    {
        $disableLocalModules = !$this->_canUseLocalModules();

        if ($mergeToObject === null) {
            $mergeToObject = clone $this->_prototype;
            $mergeToObject->loadString('<config/>');
        }
        if ($mergeModel === null) {
            $mergeModel = clone $this->_prototype;
        }
        $modules = $this->getNode('modules')->children();
        foreach ($modules as $modName=>$module) {
            if ($module->is('active')) {
                if ($disableLocalModules && ('local' === (string)$module->codePool)) {
                    continue;
                }
                if (!is_array($fileName)) {
                    $fileName = array($fileName);
                }

                foreach ($fileName as $configFile) {
                    $configFile = $this->getModuleDir('etc', $modName).DS.$configFile;
                    if ($mergeModel->loadFile($configFile)) {

                        $this->_makeEventsLowerCase(Mage_Core_Model_App_Area::AREA_GLOBAL, $mergeModel);
                        $this->_makeEventsLowerCase(Mage_Core_Model_App_Area::AREA_FRONTEND, $mergeModel);
                        $this->_makeEventsLowerCase(Mage_Core_Model_App_Area::AREA_ADMIN, $mergeModel);
                        $this->_makeEventsLowerCase(Mage_Core_Model_App_Area::AREA_ADMINHTML, $mergeModel);

                        $mergeToObject->extend($mergeModel, true);
                    }
                }
            }
        }
        return $mergeToObject;
    }

Com isso você já conseguiria trabalhar no seu módulo com suas configurações personalizadas no Magento 1.

E no Magento 2, o que muda? A resposta é: praticamente tudo!

O Magento 2 já veio com esse conceito de separar todos os arquivos de configuração para uma melhor organização e uma questão de performance, já que ele apenas carrega os arquivos necessários em determinada situação, por exemplo, se o Magento estiver sendo executado pelo terminal ou por uma cron, qualquer arquivo dentro de etc/frontend/*.xml nos diretórios dos módulos serão ignorados, pois o Magento 2 entende que uma outra área está sendo executada ao invés do frontend. Isso também acontece com o diretório etc/adminhtml/*.xml dentro dos módulos quando o Magento está executando a área frontend, área visível aos compradores (shoppers). Isso significa que criar um arquivo de configuração novo no Magento 2 requer seguir as novas práticas da plataforma, ou seja, utilizar as formas nativas que a utiliza para chegar ao resultado esperado.

Esses dias precisei criar um arquivo de configurações personalizado no Magento 2 e precisei entender todo o fluxo que ele utiliza para fazer a leitura dos mesmos nativamente. Vamos lá!

XML Config

Bom, tudo começa com seu arquivo de configuração. O próprio módulo de catálogo do Magento possui um arquivo de configurações específico. Vamos pegar este arquivo como exemplo, o arquivo catalog_attributes.xml do módulo Magento_Catalog:

<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Catalog:etc/catalog_attributes.xsd">
    <group name="catalog_category">
        <attribute name="name"/>
        <attribute name="is_active"/>
    </group>
    <group name="catalog_product">
        <attribute name="name"/>
        <attribute name="price"/>
        <attribute name="special_price"/>
        <attribute name="special_from_date"/>
        <attribute name="special_to_date"/>
        <attribute name="short_description"/>
        <attribute name="thumbnail"/>
        <attribute name="small_image"/>
        <attribute name="image_label"/>
        <attribute name="thumbnail_label"/>
        <attribute name="small_image_label"/>
        <attribute name="tax_class_id"/>
        <attribute name="status"/>
        <attribute name="news_from_date"/>
        <attribute name="news_to_date"/>
        <attribute name="created_at"/>
        <attribute name="updated_at"/>
    </group>
    <group name="unassignable">
        <attribute name="image" />
        <attribute name="name" />
        <attribute name="description" />
        <attribute name="short_description" />
        <attribute name="sku" />
        <attribute name="price" />
        <attribute name="status" />
        <attribute name="visibility" />
        <attribute name="weight" />
        <attribute name="category_ids" />
        <attribute name="quantity_and_stock_status" />
        <attribute name="available_sort_by" />
        <attribute name="default_sort_by" />
        <attribute name="filter_price_range" />
        <attribute name="image" />
        <attribute name="media_gallery_content" />
        <attribute name="tier_price" />
        <attribute name="gift_message_available" />
        <attribute name="inventory_manage_stock" />
    </group>
    <group name="used_in_autogeneration">
        <attribute name="name" />
        <attribute name="description" />
        <attribute name="sku" />
    </group>
</config>

Podemos notar que no root node deste arquivo existem dois parâmetros:

xmlns:xsi: declara um prefixo padrão para o namespace (xsi) e a URL do namespace utilizado.
xsi:noNamespaceSchemaLocation: declara o endereço do XML Schema utilizado para este arquivo. Existem dois caminhos base possíveis:

Utilizando um módulo como referência:
urn:magento:module:Vendor_ModuleName:caminho/do/arquivo.xsd

Utilizando o framework como referência:
urn:magento:framework:caminho/do/arquivo.xsd

XML Schema

Por padrão, todos os arquivos de configuração do Magento 2 precisam ser validados por um XML Schema. Isso significa que todo desenvolvedor que precisar criar arquivos novos de configurações também precisarão implementar um XML Schema para fazer a validação do arquivo XML que for criado.

Isso dá um pouquinho mais de trabalho, mas é uma forma de garantir que os novos arquivos estejam dentro do padrão esperado e, com isso, caso qualquer outro desenvolvedor queira estender as configurações de seus módulos, ele precisará também seguir estritamente as regras presentes nesse XML Schema. A probabilidade de ele dar aquela “cagadinha básica” no seu módulo por motivos de configurações erroneamente implementadas vai diminuir bastante.

Mas é preciso estar atento quando for criar XML Schemas, pois ele é um pouco mais complexo do que se imagina e pode causar uma confusão danada na sua cabeça caso ele seja extremamente grande. Tente criar arquivos simples e de fácil entendimento, se possível, documente o que for necessário para que outros desenvolvedores (ou até você mesmo no futuro) entenda os motivos das regras do arquivo.

Um exemplo de XML Schema pode ser encontrado em diversos cantos pelo Core do Magento 2.

Agora, vamos dar uma conferida no arquivo XML Schema utilizado:

<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="config">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="group" type="attributeGroupType" minOccurs="1" maxOccurs="unbounded"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:complexType name="attributeGroupType">
        <xs:sequence>
            <xs:element name="attribute" type="attributeType" minOccurs="1" maxOccurs="unbounded"/>
        </xs:sequence>
        <xs:attribute name="name" type="xs:string" use="required"/>
    </xs:complexType>

    <xs:complexType name="attributeType">
        <xs:attribute name="name" type="xs:string" use="required"/>
    </xs:complexType>
</xs:schema>

Bom, não vou entrar em detalhes sobre o XML Schema dentro deste post, pois está fora de escopo, mas o padrão dos seus arquivos de validação deve seguir o mesmo dos arquivos do core do Magento 2.

Config Converter

Agora, todo arquivo de configuração no Magento 2 precisa ter um conversor para “traduzir” seu arquivo para a plataforma. A responsabilidade desse conversor é basicamente fazer a leitura do arquivo e converter todas os XML Nodes presentes nele para um array que possa ser trabalhado internamente no seu módulo.

O conversor do XML acima é a classe Magento\Catalog\Model\Attribute\Config\Converter. Todo conversor deve implementar a interface Magento\Framework\Config\ConverterInterface e seu método convert() que irá receber uma instância de um objeto do tipo DOMDocument.

<?php
/**
 * Converter of attributes configuration from \DOMDocument to array
 *
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Catalog\Model\Attribute\Config;

class Converter implements \Magento\Framework\Config\ConverterInterface
{
    /**
     * Convert dom node tree to array
     *
     * @param \DOMDocument $source
     * @return array
     */
    public function convert($source)
    {
        $result = [];
        /** @var DOMNode $groupNode */
        foreach ($source->documentElement->childNodes as $groupNode) {
            if ($groupNode->nodeType != XML_ELEMENT_NODE) {
                continue;
            }
            $groupName = $groupNode->attributes->getNamedItem('name')->nodeValue;
            /** @var DOMNode $groupAttributeNode */
            foreach ($groupNode->childNodes as $groupAttributeNode) {
                if ($groupAttributeNode->nodeType != XML_ELEMENT_NODE) {
                    continue;
                }
                $groupAttributeName = $groupAttributeNode->attributes->getNamedItem('name')->nodeValue;
                $result[$groupName][] = $groupAttributeName;
            }
        }
        return $result;
    }
}

XML Schema Locator

Você também precisará criar um arquivo que terá a responsabilidade de localizar o XML Schema do seu novo arquivo de configurações. Essa classe implementa a interface Magento\Framework\Config\SchemaLocatorInterface e seus métodos getSchema() e getPerFileSchema(). O construtor dessa classe recebe uma instância do objeto Magento\Framework\Module\Dir\Reader, responsável por fazer a leitura do diretório de um módulo.

<?php
/**
 * Attributes config schema locator
 *
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Catalog\Model\Attribute\Config;

use Magento\Framework\Module\Dir;

class SchemaLocator implements \Magento\Framework\Config\SchemaLocatorInterface
{
    /**
     * Path to corresponding XSD file with validation rules for both individual and merged configs
     *
     * @var string
     */
    private $_schema;

    /**
     * @param \Magento\Framework\Module\Dir\Reader $moduleReader
     */
    public function __construct(\Magento\Framework\Module\Dir\Reader $moduleReader)
    {
        $this->_schema = $moduleReader->getModuleDir(Dir::MODULE_ETC_DIR, 'Magento_Catalog') . '/catalog_attributes.xsd';
    }

    /**
     * {@inheritdoc}
     */
    public function getSchema()
    {
        return $this->_schema;
    }

    /**
     * {@inheritdoc}
     */
    public function getPerFileSchema()
    {
        return $this->_schema;
    }
}

Config Data Model

Agora é preciso criar um objeto que vai prover essas configurações para outras classes quando elas precisarem. O que antes era concentrado em uma única classe no Magento 1 (Mage_Core_Model_Config) agora estão separados por tipos de arquivos. Essa classe obrigatoriamente deve apenas estender a classe Magento\Framework\Config\Data e não precisa implementar nenhum método a menos que você precise que ela faça algo mais específico. No caso abaixo o método __construct é implementado apenas para modificar o valor padrão do parâmetro $cacheId, que passa a ser ‘catalog_attributes’.

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Catalog\Model\Attribute\Config;

use Magento\Framework\Serialize\SerializerInterface;

/**
 * Provides catalog attributes configuration
 */
class Data extends \Magento\Framework\Config\Data
{
    /**
     * Constructor
     *
     * @param \Magento\Catalog\Model\Attribute\Config\Reader $reader
     * @param \Magento\Framework\Config\CacheInterface $cache
     * @param string|null $cacheId
     * @param SerializerInterface|null $serializer
     */
    public function __construct(
        \Magento\Catalog\Model\Attribute\Config\Reader $reader,
        \Magento\Framework\Config\CacheInterface $cache,
        $cacheId = 'catalog_attributes',
        SerializerInterface $serializer = null
    ) {
        parent::__construct($reader, $cache, $cacheId, $serializer);
    }
}

Dependency Injection

Por mais que o módulo não implemente nativamente, você precisa fazer algumas modificações no seu arquivo di.xml no diretório etc para que suas configurações sejam carregadas corretamente.

As configurações abaixo são referentes as classes acima, porém a mesma não existe on core do Magento 2 e estão sendo implementadas aqui apenas para fins ilustrativos.

Devem ser utilizadas apenas como base para quando você criar suas próprias configurações, portanto, atente-se na hora de fazer as devidas substituições on arquivo.

<virtualType name="CatalogAttributeReader" type="Magento\Framework\Config\Reader\Filesystem">
    <arguments>
        <argument name="converter" xsi:type="object”>Magento\Catalog\Model\Attribute\Config\Converter</argument>
        <argument name="schemaLocator" xsi:type="object">Magento\Catalog\Model\Attribute\Config\SchemaLocator</argument>
        <argument name="fileName" xsi:type="string”>catalog_attributes.xml</argument>
    </arguments>
</virtualType>
<type name="Magento\Catalog\Model\Attribute\Config\Data">
    <arguments>
        <argument name="reader" xsi:type="object">CatalogAttributeReader</argument>
        <argument name="cacheId" xsi:type="string”>catalog_attributes_cache</argument>
    </arguments>
</type>

Utilizando Suas Configurações

E dessa forma poderemos utilizar nossas configurações personalizadas sempre que necessário simplesmente injetando nossa classe no construtor de qualquer outra classe:

use Magento\Catalog\Model\Attribute\Config\Data as AttributesConfigData

class CustomAttributes
{
    /** @var AttributesConfigData $attributesConfig */
    protected $attributesConfigData;
    
    /** @var AttributesConfigData $attributesConfigData */
    public function __construct(AttributesConfigData $attributesConfigData)
    {
        $this->attributesConfigData = $attributesConfigData;
    }
    
    public function getAttributes()
    {
        return $this->attributesConfigData->get('attributes');
    }
}

Conclusão

Bom, agora podemos ver que no Magento 2 a coisa ficou séria! Em comparação ao Magento 1 a criação de um novo arquivo de configuração personalizado envolve, ao menos, 6 arquivos: arquivo de configurações, XML Schema, Converter, Schema Locator, Config Data Model e uma alteração no arquivo di.xml.

Se você achou um pouco frustrante ter que lembrar de tudo isso quando você precisar criar um novo arquivo de configuração para seu módulo não fique triste. Tenho trabalhado no Magentor para poder facilitar a minha e a vida de vocês nessas horas. Ainda não conhece o Magentor? Então a primeira coisa é ficar atento na página oficial, por lá você tem acesso ao repositório no Github, e depois ler um pouco sobre o futuro da ferramenta no artigo que criei em meu blog pessoal.

Com o Magentor será possível criar cada arquivo citado aqui neste post separadamente ou criar-los de uma única vez. Você só precisará implementar as regras de negócio, não é fantástico!!?? O projeto ainda está em tempo de desenvolvimento, mas em breve terá seu primeiro release e você poderá utilizar a ferramenta gratuitamente.

Espero que este artigo tenha lhe ajudado a entender um pouco sobre como funcionam as configurações personalizadas no Magento 2. Em breve trarei novas abordagens, pois tem muita coisa nova para ser discutido.

Um grande abraço e até a próxima!