Back home
Mark Shust

Written by Mark Shust, a Certified Magento Developer, Architect & Teacher in Cleveland, Ohio.
Follow me @markshust

Create a product attribute data patch with Magento 2.3's declarative schema

February 19, 2019   ·   5 min read  ·   Edit on GitHub

I was refactoring one of my Magento 2 modules and noticed that the Magento 2.3 core modules use the declarative schema approach rather than setup upgrade scripts. This is the new recommended approach for Magento versions 2.3 and up, as upgrade scripts will be phased out in favor of this declarative schema approach in the future.

I stumbled on the data patches documentation, however this doesn’t really apply for creating product attributes within declarative schema scripts.

First, we need to create a class that implements DataPatchInterface, and instantiate a copy of the EavSetupFactory class within the constructor.

The naming convention Magento core files use for modifications to attributes within declarative schema scripts is: Verb + (Name or Explanation) + Attribute(s). So, if we are trying to add a single attribute named alternate_color, we name our class AddAlternateColorAttribute.

Within your Setup\Patch\Data folder, create a new file named AddAlternativeColorAttribute.php with the contents:

<?php
namespace Acme\Foo\Setup\Patch\Data;

use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;

class AddAlternativeColorAttribute implements DataPatchInterface
{
    /** @var ModuleDataSetupInterface */
    private $moduleDataSetup;

    /** @var EavSetupFactory */
    private $eavSetupFactory;

    /**
     * @param ModuleDataSetupInterface $moduleDataSetup
     * @param EavSetupFactory $eavSetupFactory
     */
    public function __construct(
        ModuleDataSetupInterface $moduleDataSetup,
        EavSetupFactory $eavSetupFactory
    ) {
        $this->moduleDataSetup = $moduleDataSetup;
        $this->eavSetupFactory = $eavSetupFactory;
    }
}

The DataPatchInterface expects the implementation of three functions: apply, getDependencies and getAliases.

The apply function is where we will create our attribute items. Since we are only creating attributes, there is no need to call startSetup and endSetup functions here anymore. We just create an instance of the EavSetupFactory, passing in our moduleDataSetup object, and add our attribute:

    /**
     * {@inheritdoc}
     */
    public function apply()
    {
        /** @var EavSetup $eavSetup */
        $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);

        $eavSetup->addAttribute('catalog_product', 'alternative_color', [
            'type' => 'int',
            'label' => 'Alternative Color',
            'input' => 'select',
            'used_in_product_listing' => true,
            'user_defined' => true,
        ]);
    }

Note that just about all attribute parameters except type, label, and input are optional here, and we should really only define the properties which differ from default settings & values. In this case, we are creating a select dropdown and we want to set user_defined to true so a user can add values to this attribute from the admin. We’ll also toggled used_in_product_listing to true so we have access to this attribute within the product listing database query.

The getDependencies function expects an array of strings containing class names of dependencies. This is new functionality specific to declarative schema scripts, and tells Magento to execute the “patches” we define here first, before our setup script. This is how Magento controls the order of how patch scripts are executed.

In this situation, we won’t have any dependencies, so we’ll just return an empty array:

    /**
     * {@inheritdoc}
     */
    public static function getDependencies()
    {
        return [];
    }

The last function getAliases, which defines aliases for this patch class. Since we don’t really specify version numbers anymore, our class name could change, and if it does, we should supply the old class name here so it’s not executed a second time (patches are only ever ran once). Since this is a new script, we won’t have any aliases, so we’ll again return an empty array:

    /**
     * {@inheritdoc}
     */
    public function getAliases()
    {
        return [];
    }

One last bonus that we won’t really use, but I think it’s worth mentioning… if we specify a getVersion function, we can return a string with a version number. If the version number of the module in our database is higher than the version we specify here in our file, the patch will not execute. If it is equal to or lower than the version here, it will execute. It would seem to me that best practices would denote to not use the versioning capabilities at all, however there are certainly specific situations which will warrant versioning, possibly with complex installations or specific requirements. The format would be as follows:

    /**
     * {@inheritdoc}
     */
    public static function getVersion()
    {
        return '2.0.6';
    }

All that explaned, here is our final class:

app/code/Acme/Foo/Setup/Patch/Data/AddAlternativeColorAttribute.php
<?php
namespace Acme\Foo\Setup\Patch\Data;

use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;

class AddAlternativeColorAttribute implements DataPatchInterface
{
    /** @var ModuleDataSetupInterface */
    private $moduleDataSetup;

    /** @var EavSetupFactory */
    private $eavSetupFactory;

    /**
     * @param ModuleDataSetupInterface $moduleDataSetup
     * @param EavSetupFactory $eavSetupFactory
     */
    public function __construct(
        ModuleDataSetupInterface $moduleDataSetup,
        EavSetupFactory $eavSetupFactory
    ) {
        $this->moduleDataSetup = $moduleDataSetup;
        $this->eavSetupFactory = $eavSetupFactory;
    }

    /**
     * {@inheritdoc}
     */
    public function apply()
    {
        /** @var EavSetup $eavSetup */
        $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);

        $eavSetup->addAttribute('catalog_product', 'alternative_color', [
            'type' => 'int',
            'label' => 'Alternative Color',
            'input' => 'select',
            'used_in_product_listing' => true,
            'user_defined' => true,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public static function getDependencies()
    {
        return [];
    }

    /**
     * {@inheritdoc}
     */
    public function getAliases()
    {
        return [];
    }
}

Now when we run bin/magento setup:upgrade to apply the updates, our data patch executes and the attribute is created. For all patches which are successfully executed, Magento inserts a record into the patch_list database table with the value of the patch_name field being the value of our patch class, like so:

patch_list
patch_id    patch_name
...
126         Magento\WidgetSampleData\Setup\Patch\Data\InstallWidgetSampleData
127         Magento\WishlistSampleData\Setup\Patch\Data\InstallWishlistSampleData
128         Acme\Foo\Setup\Patch\Data\AddAlternativeColorAttribute

Removing the value from the patch_list table will cause the patch to re-execute when running bin/magento setup:upgrade again, so this approach can be extremely useful when first creating and debugging patch scripts.

Share On Twitter
Let others know about this article

Learning Magento?

I'll send out tidbits (not more than once a week) explaining Magento 2 concepts to beginners.

    I won't send you spam. Unsubscribe at any time.