One of the solution is to add a backend model
to your attribute which is used to format / validate your attribute value before save and/or after load.
Add a backend class :
[
'type' => 'int',
'backend' => '\Foo\Bar\Model\Attribute\Backend\YourAttribute',
'frontend' => '',
'label' => 'XXXX',
'input' => 'text',
'frontend_class' => 'validate-greater-than-zero',
'source' => '',
'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL,
'visible' => true,
'required' => true,
'user_defined' => false,
'default' => 0,
'searchable' => false,
'filterable' => true,
'comparable' => false,
'visible_on_front' => false,
'used_in_product_listing' => true,
'unique' => false
]
Here is an example of your custom class \Foo\Bar\Model\Attribute\Backend\YourAttribute
<?php
namespace Foo\Bar\Model\Attribute\Backend;
/**
* Class YourAttribute
*/
class YourAttribute extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
{
/**
* @var int $minimumValueLength
*/
protected $minimumValueLength = 0;
/**
* @param \Magento\Framework\DataObject $object
*
* @return $this
*/
public function afterLoad($object)
{
// your after load logic
return parent::afterLoad($object);
}
/**
* @param \Magento\Framework\DataObject $object
*
* @return $this
*/
public function beforeSave($object)
{
$this->validateLength($object);
return parent::beforeSave($object);
}
/**
* Validate length
*
* @param \Magento\Framework\DataObject $object
*
* @return bool
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function validateLength($object)
{
/** @var string $attributeCode */
$attributeCode = $this->getAttribute()->getAttributeCode();
/** @var int $value */
$value = (int)$object->getData($attributeCode);
/** @var int $minimumValueLength */
$minimumValueLength = $this->getMinimumValueLength();
if ($this->getAttribute()->getIsRequired() && $value <= $minimumValueLength) {
throw new \Magento\Framework\Exception\LocalizedException(
__('The value of attribute "%1" must be greater than %2', $attributeCode, $minimumValueLength)
);
}
return true;
}
/**
* Get minimum attribute value length
*
* @return int
*/
public function getMinimumValueLength()
{
return $this->minimumValueLength;
}
}
If you want a simple example of that kind of class you can check
\Magento\Customer\Model\Customer\Attribute\Backend\Website
- all the classes which extend
\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
- the classes into
backend_model
column in eav_attribute
table
EDIT
If you want a class that do nearly the same thing as you want you can take a look at the
SKU
attribute validation
\Magento\Catalog\Model\Product\Attribute\Backend\Sku
I also added the method in the example class
EDIT
Another solution (maybe not the best one) is to create a plugin on the function
\Magento\Eav\Helper\Data::getFrontendClasses
and add your frontend class here that can be validated in front.
customer
and customer_address
Does not support Scope Attribute
[Global]
catalog_category
and catalog_product
Does support Scope Attribute
[Default, Website and Store]
So the question is, Sales tables does support Scope Attributes? Because
sales tables are stored as Flat tables in Magento
Sales DOES NOT support Scope Attributes.
Confirmation 1 : Check the eav_entity_type
table in Magento 2
attribute_model
and entity_attribute_collection
is NULL for
sales related entities.
- Where as Customer Attributes can be created Scope wise.
- Catalog Attributes can be created Scope wise.
Confirmation 2 : Check getDefaultEntities
function in Setup
folder, It clears the doubt.
vendor/magento/module-customer/Setup/CustomerSetup.php
/**
* Retrieve default entities: customer, customer_address
*
* @return array
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function getDefaultEntities()
{
$entities = [
'customer' => [
'entity_type_id' => \Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER,
'entity_model' => \Magento\Customer\Model\ResourceModel\Customer::class,
'attribute_model' => \Magento\Customer\Model\Attribute::class,
'table' => 'customer_entity',
'increment_model' => \Magento\Eav\Model\Entity\Increment\NumericValue::class,
'additional_attribute_table' => 'customer_eav_attribute',
'entity_attribute_collection' => \Magento\Customer\Model\ResourceModel\Attribute\Collection::class,
'attributes' => [
'website_id' => [
'type' => 'static',
'label' => 'Associate to Website',
'input' => 'select',
'source' => \Magento\Customer\Model\Customer\Attribute\Source\Website::class,
'backend' => \Magento\Customer\Model\Customer\Attribute\Backend\Website::class,
'sort_order' => 10,
'position' => 10,
'adminhtml_only' => 1,
],
'store_id' => [
'type' => 'static',
'label' => 'Create In',
'input' => 'select',
'source' => \Magento\Customer\Model\Customer\Attribute\Source\Store::class,
'backend' => \Magento\Customer\Model\Customer\Attribute\Backend\Store::class,
'sort_order' => 20,
'visible' => false,
'adminhtml_only' => 1,
],
vendor/magento/module-catalog/Setup/CategorySetup.php
/**
* Default entities and attributes
*
* @return array
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function getDefaultEntities()
{
return [
'catalog_category' => [
'entity_type_id' => self::CATEGORY_ENTITY_TYPE_ID,
'entity_model' => Category::class,
'attribute_model' => Attribute::class,
'table' => 'catalog_category_entity',
'additional_attribute_table' => 'catalog_eav_attribute',
'entity_attribute_collection' =>
Collection::class,
'attributes' => [
'name' => [
'type' => 'varchar',
'label' => 'Name',
'input' => 'text',
'sort_order' => 1,
'global' => ScopedAttributeInterface::SCOPE_STORE,
'group' => 'General Information',
],
vendor/magento/module-sales/Setup/SalesSetup.php
/**
* @return array
*/
public function getDefaultEntities()
{
$entities = [
'order' => [
'entity_type_id' => self::ORDER_ENTITY_TYPE_ID,
'entity_model' => \Magento\Sales\Model\ResourceModel\Order::class,
'table' => 'sales_order',
'increment_model' => \Magento\Eav\Model\Entity\Increment\NumericValue::class,
'increment_per_store' => true,
'attributes' => [],
],
'invoice' => [
'entity_type_id' => self::INVOICE_PRODUCT_ENTITY_TYPE_ID,
'entity_model' => \Magento\Sales\Model\ResourceModel\Order\Invoice::class,
'table' => 'sales_invoice',
'increment_model' => \Magento\Eav\Model\Entity\Increment\NumericValue::class,
'increment_per_store' => true,
'attributes' => [],
],
'creditmemo' => [
'entity_type_id' => self::CREDITMEMO_PRODUCT_ENTITY_TYPE_ID,
'entity_model' => \Magento\Sales\Model\ResourceModel\Order\Creditmemo::class,
'table' => 'sales_creditmemo',
'increment_model' => \Magento\Eav\Model\Entity\Increment\NumericValue::class,
'increment_per_store' => true,
'attributes' => [],
],
'shipment' => [
'entity_type_id' => self::SHIPMENT_PRODUCT_ENTITY_TYPE_ID,
'entity_model' => \Magento\Sales\Model\ResourceModel\Order\Shipment::class,
'table' => 'sales_shipment',
'increment_model' => \Magento\Eav\Model\Entity\Increment\NumericValue::class,
'increment_per_store' => true,
'attributes' => [],
],
];
return $entities;
}
In Conformation 2 - getDefaultEntities
is Scope wise for Customer Attributes and Catalog as well Categories But Sales DOES NOT support Scope Attributes, It is not defined in Setup.
Best Answer
Ok, this seems to be a bug: https://github.com/magento/magento2/issues/13440
Solution (worked for me): https://github.com/magento/magento2/issues/13440#issuecomment-436712363
\app\code\Vendor\Module\etc\di.xml
\app\code\Vendor\Module\Plugin\Model\Category\DataProvider.php:
Followed by a
bin/magento setup:di:compile