Custom customer attribute in Magento 2


I recently had the opportunity to build a module that will add custom customer attribute in Magento 2 and decided to note down some interesting facts I noticed during development.

I will not go into any depth on basic module building, rather I want to point out some things I find interesting to keep in mind while building similar modules.

The idea here was not only to simply add a new attribute, but to prepare the module for further extending and the addition of more attributes, more easily, should there be a need to do so in the future.

For now, let’s just start by adding a simple varchar attribute in our install script:

<?php


namespace CustomAttributes\Customers\Setup;

use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Customer\Model\Customer;
use Magento\Customer\Setup\CustomerSetupFactory;

class InstallData implements InstallDataInterface
{

    private $customerSetupFactory;

    /**
     * Constructor
     *
     * @param \Magento\Customer\Setup\CustomerSetupFactory $customerSetupFactory
     */
    public function __construct(
        CustomerSetupFactory $customerSetupFactory
    ) {
        $this->customerSetupFactory = $customerSetupFactory;
    }

    /**
     * {@inheritdoc}
     */
    public function install(
        ModuleDataSetupInterface $setup,
        ModuleContextInterface $context
    ) {
        $customerSetup = $this->customerSetupFactory->create(['setup' => $setup]);

        $customerSetup->addAttribute(\Magento\Customer\Model\Customer::ENTITY, 'github_url', [
            'type' => 'varchar',
            'label' => 'GitHub Url',
            'input' => 'text',
            'source' => '',
            'required' => false,
            'visible' => true,
            'position' => 1,
            'system' => false,
            'frontend_class' => 'validate-length maximum-length-200 validate-url',
            "unique"     => true,
            'backend' => 'CustomAttributes\Customers\Model\Attribute\Backend\Github'
        ]);


        $attribute = $customerSetup->getEavConfig()->getAttribute('customer', 'github_url')
            ->addData([
                'used_in_forms' => [
                    'adminhtml_customer',
                    'adminhtml_checkout',
                    'customer_account_create',
                    'customer_account_edit'
                ]
            ]);
        $attribute->save();
    }
}

If we examine the 3rd parameter of the addAttribute method, we will notice some validation classes I added for validation purposes. This is a really cool feature and you can find a full list of possible validations in official Magento documentation.

Validations on frontend are useful and cool, in some cases, while in others you also need backend validations. Let’s say we want to make sure that each record is unique.

At first, I tried to achieve this by adding “1” as a value to the isUnique field in my eav_attribute, however, as this wasn’t working at all, I was still able to save the same value to multiple customers.

I decided to add a custom backend model to my attribute and add my validation there. Adding a custom backend model, for custom attributes, is a nice and elegant way of customizing behavior. While observers may be necessary sometimes, in this case, we can use a backend model.

Let’s have a look at the backend model:

<?php

namespace CustomAttributes\Customers\Model\Attribute\Backend;

use Magento\Customer\Model\Customer;
use Magento\Framework\Exception\LocalizedException;

class Github extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
{
    /**
     * Validate github_url
     *
     * @param Customer $object
     * @return bool
     * @throws \Magento\Framework\Exception\LocalizedException
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function validate($object)
    {
        $attribute = $this->getAttribute();
        $attrCode = $attribute->getAttributeCode();
        $value = $object->getData($attrCode);

        if ($attribute->getIsVisible()
            && $attribute->getIsRequired()
            && $attribute->isValueEmpty($value)
            && $attribute->isValueEmpty($attribute->getDefaultValue())
        ) {
            throw new LocalizedException(__('The value of attribute "%1" must be set', $attrCode));
        }

        if ($attribute->getIsUnique()
            && !$attribute->getIsRequired()
            && ($value == '' || $attribute->isValueEmpty($value))
        ) {
            return true;
        }

        if ($attribute->getIsUnique()) {
            if (!$attribute->getEntity()->checkAttributeUniqueValue($attribute, $object)) {
                $label = $attribute->getFrontend()->getLabel();
                throw new LocalizedException(__('The value of attribute "%1" must be unique', $label));
            }
        }

        return true;
    }

    /**
     * Validate github_url
     *
     * @param Customer $object
     * @return bool
     * @throws \Magento\Framework\Exception\LocalizedException
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function beforeSave($object)
    {
        $this->validate($object);
    }
}

I call the validate method before saving the attribute and do all the validations there. In this case, I didn’t edit the validate method, as it already had what I needed, I just called it before saving, to fix the timing, when it should be called and prevent unwanted behavior.

The validate method is the perfect spot for adding custom validations and when it already has a lot of useful validations in it, by default.

Now that the attribute is in place, we can proceed with organizing the model and view of the module. This is where we can optimize code a bit more, in order to make the future custom attributes easier to add.

Even if the specification states that only one new attribute should be added, I always like to leave everything prepared for more, because more often than not, more is required and plans are extended.

So, let’s build our model:

<?php

namespace CustomAttributes\Customers\Model;

use Magento\Eav\Model\Entity\Attribute;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Model\Session;

class CustomAttributes
{
    protected $entityAttribute;
    protected $customerSession;
    protected $customerRepository;

    const GITHUB_ATTR_CODE = 'github_url';

    public function __construct(
        Session $customerSession,
        CustomerRepositoryInterface $customerRepository,
        Attribute $entityAttribute
    ) {
        $this->entityAttribute = $entityAttribute;
        $this->customerSession = $customerSession;
        $this->customerRepository = $customerRepository;
    }

    protected function getCustomer()
    {
        return $this->customerRepository->getById($this->customerSession->getCustomerId());
    }

    public static function getGithubCode()
    {
        return self::GITHUB_ATTR_CODE;
    }

    protected function customAttribute($attributeCode)
    {
        $attributeData = $this->entityAttribute->loadByCode('customer', $attributeCode);
        $customerLoggedIn = $this->customerSession->isLoggedIn();
        $customerValue = '';

        if ($customerLoggedIn) {
            $customerAttribute = $this->getCustomer()->getCustomAttribute($attributeCode);
            if (isset($customerAttribute)) {
                $customerValue = $customerAttribute->getValue();
            }
        }

        return array(
            'attribute_label' => $attributeData->getFrontendLabel(),
            'validations' => $attributeData->getFrontendClass(),
            'input' => $attributeData->getFrontendInput(),
            'customer_value' => $customerValue
        );
    }

    /**
     * Returns an array with all the custom attributes we want to add
     * to the customer front end forms (create and edit).
     *
     * Convenient if we need to add more custom attributes later.
     */
    public function templateAttributes()
    {
        return array
        (
            $this->getGithubCode() => $this->customAttribute($this->getGithubCode())
        );
    }
}

The method that will be used in views is templateAttributes. Instead of returning data on one attribute, let’s immediately prepare and organize everything for more than just one attribute.

Load all attribute data that will be required in the views and return an array, with attribute codes as keys and data as value.

Once we have this in place, we can do something like this in our views:

<?php $printAttributes = $this->getTemplateAttributes(); ?>

<fieldset class="fieldset additional data">
    <legend class="legend">
        <span>
            <?php echo __('Additional Information'); ?>
        </span>
    </legend>
    <br>
    <?php foreach ($printAttributes as $attributeCode => $attributeData): ?>
    <div class="field <?php echo $attributeCode; ?>">
        <label for="<?php echo $attributeCode; ?>" class="label">
            <span><?php echo __($attributeData['attribute_label']); ?>
            </span>
        </label>
        <div class="control">
            <input type="<?php echo $attributeData['input']; ?>"
                   name="<?php echo $attributeCode; ?>"
                   id="<?php echo $attributeCode; ?>"
                   title="<?php echo __($attributeData['attribute_label']); ?>"
                   class="input-<?php echo $attributeData['input'] . ' ' . $attributeData['validations']; ?>"
                   value="<?php echo $attributeData['customer_value']; ?>">
        </div>
    </div>
    <?php endforeach; ?>
</fieldset>

These were some interesting things I noticed while building this feature, and I wish I had known some of them before I started, so I hope this will be useful for someone starting out with a similar project.

If you have a question, a comment, or you need the full module to bootstrap your own project, please feel free to contact me.

+ There are no comments

Add yours