Using composition in patches / install scripts
-
07-03-2021 - |
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.
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;
}
....
}