Question

TL;DR: Is there a way to declare virtual types and different arguments in di.xml of a module and have that configuration be picked up when the module gets installed without calling bin/magento module:enable Vendor_Module prior to installing it?

Long version.
I have the following task.
I need to import some attributes from a source when a certain module is installed. (Please do not recommend me extensions for importing attributes. This is not the problem itself. It's just an example of a bigger problem.) For this I've created a data install patch that looks kind of like this (it is actually bigger than that, but I left only what's important):

<?php
namespace Vendor\Module\Setup\Patch\Data;

use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Catalog\Model\Product;
use Magento\Eav\Setup\EavSetup;

class ImportAttributes implements DataPatchInterface
{
    /**
     * @var EavSetup
     */
    private $eavSetup;

    /**
     * ImportAttributes constructor.
     * @param EavSetup $eavSetup
     */
    public function __construct(
        EavSetup $eavSetup
    ) {
        $this->eavSetup = $eavSetup;
    }

    /**
     * {@inheritdoc}
     */
    public function apply(): void
    {
        $attributes = $this->getImportData();
        foreach ($attributes as $attributeCode => $attributeData) {
            $this->eavSetup->addAttribute(Product::ENTITY, $attributeCode, $attributeData);
        }
    }

    /**
     * @return array
     */
    private function getImportData(): array
    {
        //get data from somewhere 
        return $attributeArray;
    }
 ....
}

Pretty simple and it works nicely.
Now the problem.
The addAttribute method I'm calling inside the apply method contains this:

    $data = array_replace(
        ['entity_type_id' => $entityTypeId, 'attribute_code' => $code],
        $this->attributeMapper->map($attr, $entityTypeId)
    );

This basically transforms the attribute data to a certain format that is used to insert the attributes in db later.
My data source sends me the attributes already formatted as they should and applying the effects of $this->attributeMapper->map leads to undesired results.

I was able to solve this again easily by creating an attribute mapper (implementation of interface Magento\Eav\Model\Entity\Setup\PropertyMapperInterface) that just returns what it receives.
And I declared that via di.xml

<type name="Vendor\Module\Setup\Patch\Data">
    <arguments>
        <argument name="eavSetup" xsi:type="object">VirtualEavSetup</argument>
    </arguments>
</type>
<virtualType name="VirtualEavSetup" type="Magento\Eav\Setup\EavSetup">
    <arguments>
        <argument name="context" xsi:type="object">VirtualEavSetupContext</argument>
    </arguments>
</virtualType>
<virtualType name="VirtualEavSetupContext" type="Magento\Eav\Model\Entity\Setup\Context">
    <arguments>
        <argument name="attributeMapper" xsi:type="object">Vendor\Module\Model\Attribute\Mapper</argument>
    </arguments>
</virtualType>

This works great, but only if I call php bin/magento module:enable Vendor_Module prior to calling php bin/magento setup:upgrade.
If I don't enable the module first my di.xml file is not taken into account when running the patch.

Is there a way to be able to take into account the di.xml of a module when it gets installed without enabling it first.
I need this because not everyone that installs the module will enable it first and this may lead to strange results.

Was it helpful?

Solution

I didn't find a solution to use virtual types in patch scripts. Instead I had to do every object instantiation from the patch class itself.
Something like this.

<?php
declare(strict_types=1);

namespace Vendor\Module\Setup\Patch\Data;

use Magento\Framework\Setup\Patch\DataPatchInterface;
use Vendor\Module\Model\Attribute\Mapper;
use Magento\Catalog\Model\Product;
use Magento\Eav\Model\Entity\Setup\Context;
use Magento\Eav\Model\Entity\Setup\ContextFactory;
use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;

class Attribute implements DataPatchInterface
{
    /**
     * @var EavSetupFactory
     */
    private $eavSetupFactory;
    /**
     * @var Mapper
     */
    private $attributeMapper;
    /**
     * @var ContextFactory
     */
    private $eavSetupContextFactory;
    /**
     * @var EavSetup
     */
    private $eavSetup;

    /**
     * Attribute constructor.
     * @param EavSetupFactory $eavSetupFactory
     * @param Mapper $attributeMapper
     * @param ContextFactory $eavSetupContextFactory
     */
    public function __construct(
        EavSetupFactory $eavSetupFactory,
        Mapper $attributeMapper,
        ContextFactory $eavSetupContextFactory
    ) {
        $this->eavSetupFactory = $eavSetupFactory;
        $this->attributeMapper = $attributeMapper;
        $this->eavSetupContextFactory = $eavSetupContextFactory;
    }

    /**
     * @param array $attributes
     * @throws \Magento\Framework\Exception\LocalizedException
     * @throws \Zend_Validate_Exception
     */
    public function apply(array $attributes): void
    {
        $attributes = $this->getImportData();
        foreach ($attributes as $attributeCode => $attributeData) {
            $this->getEavSetup()->addAttribute(Product::ENTITY, $attributeCode, $attributeData);
        }
    }

    /**
     * @return EavSetup
     */
    private function getEavSetup(): EavSetup
    {
        if ($this->eavSetup === null) {
            /** @var Context $context */
            $context = $this->eavSetupContextFactory->create(['attributeMapper' => $this->attributeMapper]);
            $this->eavSetup = $this->eavSetupFactory->create(['context' => $context]);
        }
        return $this->eavSetup;
    }
    /**
     * @return array
     */
    private function getImportData(): array
    {
        //get data from somewhere 
        return $attributeArray;
    }
     ....
}
Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top