Как добавить пользовательский атрибут продукта в качестве фильтра для тарифов на доставку
Опубликовано: 2020-04-07Часто в Magento 2 количество стандартных атрибутов товара, которые можно использовать для настройки условий, ограничено. Для удовлетворения потребностей бизнеса потребуется дополнительная настройка.
Из этой статьи вы узнаете, как этого добиться и добавить пользовательские атрибуты продукта в качестве фильтра для тарифов на доставку.
Заметки:
- См. полный пример кода на GitHub.
- Первая часть примера, в которой атрибут «Объемный вес» добавляется в качестве фильтра к тарифам на доставку, доступна здесь.
- Требуется оригинальный модуль Magento 2 Shipping Suite Ultimate.
Перейдем непосредственно к обсуждению того, что именно нужно сделать для достижения цели.
Оглавление
- Пошаговое руководство по добавлению пользовательского атрибута продукта
- Шаг 1. Создайте новый модуль, добавив базовые файлы
- Шаг 2. Создайте структуру модуля
- Шаг 3. Пользовательский интерфейс
- Форма
- Сетка
Пошаговое руководство по добавлению пользовательского атрибута продукта
Шаг 1. Создайте новый модуль, добавив базовые файлы
Начните с названия модуля:
> app/code/MageWorx/ShippingRateByProductAttribute/registration.php <?php /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ \Magento\Framework\Component\ComponentRegistrar::register( \Magento\Framework\Component\ComponentRegistrar::MODULE, 'MageWorx_ShippingRateByProductAttribute', __DIR__ );
Затем определите и объявите его требования. Вам также нужно будет дать имя нашему модулю для композитора, установить версию и добавить краткое описание:
> app/code/MageWorx/ShippingRateByProductAttribute/composer.json { "name": "mageworx/module-shipping-rate-by-product-attribute", "description": "Shipping Rules Extension: Adds product attribute to the Rates", "require": { "magento/module-shipping": ">=100.1.0 < 101", "magento/module-ui": ">=100.1.0 < 102", "mageworx/module-shippingrules": ">=2.7.1" }, "type": "magento2-module", "version": "1.0.0", "license": [ "OSL-3.0", "AFL-3.0" ], "autoload": { "files": [ "registration.php" ], "psr-4": { "MageWorx\\ShippingRateByProductAttribute\\": "" } } }
Далее мы можем задать начальное имя и версию в конфигурации Magento 2, объявить последовательность:
> app/code/MageWorx/ShippingRateByProductAttribute/etc/module.xml <?xml version="1.0"?> <!-- /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="MageWorx_ShippingRateByProductAttribute" setup_version="1.0.0"> <sequence> <module name="MageWorx_ShippingRules" /> </sequence> </module> </config>
Шаг 2. Создайте структуру модуля
Предположим, что у нас есть атрибут продукта с именем «shippingnew», который был создан со стороны администратора. Это тип ввода с раскрывающимся списком и несколько вариантов с именами «A, B, C, D» и т. д. Эти параметры описывают, как мы отправляем наши товары по зонам. Каждое значение имеет свою цену, и продукты с самой высокой ценой изменят стоимость способа доставки во время оформления заказа.
Прежде всего, нам нужно создать отдельную таблицу для наших расширенных условий тарифов на доставку. Позже мы добавим их, используя обычные атрибуты расширения модели (модель «Стоимость доставки» расширяет `\Magento\Framework\Model\AbstractExtensibleModel`).
> app/code/MageWorx/ShippingRateByProductAttribute/Setup/InstallSchema.php <?php /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ namespace MageWorx\ShippingRateByProductAttribute\Setup; use Magento\Framework\DB\Ddl\Table; use Magento\Framework\Setup\InstallSchemaInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\SchemaSetupInterface; /** * Class InstallSchema */ class InstallSchema implements InstallSchemaInterface { /** * Installs DB schema for a module * * @param SchemaSetupInterface $setup * @param ModuleContextInterface $context * @return void * @throws \Zend_Db_Exception */ public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) { $installer = $setup; $installer->startSetup(); $ratesTable = $installer->getTable(\MageWorx\ShippingRules\Model\Carrier::RATE_TABLE_NAME); /** * Create table 'mageworx_shippingrules_rates_shippingnew' */ $table = $installer->getConnection()->newTable( $installer->getTable('mageworx_shippingrules_rates_shippingnew') )->addColumn( 'rate_id', Table::TYPE_INTEGER, null, ['unsigned' => true, 'nullable' => false], 'Rate Id' )->addColumn( 'shippingnew', Table::TYPE_TEXT, '120', ['nullable' => false], 'shippingnew attribute value' )->addForeignKey( $installer->getFkName('mageworx_shippingrules_rates_shippingnew', 'rate_id', $ratesTable, 'rate_id'), 'rate_id', $ratesTable, 'rate_id', Table::ACTION_CASCADE )->addIndex( $installer->getIdxName( 'mageworx_shippingrules_rates_product_attributes', ['rate_id', 'shippingnew'], \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE ), ['rate_id', 'shippingnew'], ['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE] )->setComment( 'Product Attributes For Shipping Suite Rates' ); $installer->getConnection()->createTable($table); } }
Мы назвали нашу таблицу следующим образом: `'mageworx_shippingrules_rates_shippingnew'`. Там всего 2 столбца. Один из них используется как внешний ключ. Это столбец `rate_id`, который будет связан с обычной таблицей `mageworx_shippingrules_rates` из модуля MageWorx Shipping Suite Ultimate для Magento 2. Другой столбец будет содержать значения из атрибута `shippingnew`.
Прежде чем мы заставим наблюдателя загружать/сохранять/удалять наши пользовательские данные в таблицу, мы должны создать как минимум две модели — обычную модель и модель ресурсов.
> app/code/MageWorx/ShippingRateByProductAttribute/Model/ShippingNew.php <?php /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ namespace MageWorx\ShippingRateByProductAttribute\Model; use Magento\Framework\Model\AbstractModel; /** * Class ShippingNew */ class ShippingNew extends AbstractModel { /** * Prefix of model events names * * @var string */ protected $_eventPrefix = 'mageworx_shippingnew'; /** * Parameter name in event * * In observe method you can use $observer->getEvent()->getObject() in this case * * @var string */ protected $_eventObject = 'shippingnew'; /** * Set resource model and Id field name * * @return void */ protected function _construct() { parent::_construct(); $this->_init('MageWorx\ShippingRateByProductAttribute\Model\ResourceModel\ShippingNew'); $this->setIdFieldName('rate_id'); } }
Заметки:
- ` _eventPrefix` будет использоваться для обнаружения событий нашей модели.
- `_eventObject` будет использоваться для хранения данных в объекте события. Используя это имя, мы можем получить нашу модель из объекта события.
- `$this->_init( 'MageWorx\ShippingRateByProductAttribute\Model\ResourceModel\ ShippingNew' );` связывает нашу модель с соответствующей моделью ресурсов.
- `$this->setIdFieldName( 'rate_id' );` описывает, какое поле из таблицы должно быть использовано в качестве ключа (обычно мы называем его id)
> app/code/MageWorx/ShippingRateByProductAttribute/Model/ResourceModel/ShippingNew.php <?php /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ namespace MageWorx\ShippingRateByProductAttribute\Model\ResourceModel; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; /** * Class ShippingNew */ class ShippingNew extends AbstractDb { /** * Resource initialization * * @return void */ protected function _construct() { $this->_init('mageworx_shippingrules_rates_shippingnew', 'rate_id'); } /** * @param $rateId * @param int $shippingNew * @return int * @throws \Magento\Framework\Exception\LocalizedException */ public function insertUpdateRecord($rateId, int $shippingNew) { $rowsAffected = $this->getConnection()->insertOnDuplicate( $this->getMainTable(), [ 'rate_id' => $rateId, 'shippingnew' => $shippingNew ] ); return $rowsAffected; } /** * @param $rateId * @return int * @throws \Magento\Framework\Exception\LocalizedException */ public function deleteRecord($rateId) { $rowsAffected = $this->getConnection()->delete( $this->getMainTable(), [ 'rate_id = ?' => $rateId ] ); return $rowsAffected; } }
Заметки:
- $this->_init( 'mageworx_shippingrules_rates_shippingnew' , 'rate_id' ); установить имя основной таблицы и имя поля идентификатора.
- public function insertUpdateRecord($rateId, int $shippingNew) — это метод, который может помочь нам обновить значение атрибута в нашей пользовательской таблице.
- публичная функция deleteRecord($rateId) предназначена для удаления столбца.
Позже мы будем использовать эти методы в наших наблюдателях.
Теперь давайте добавим наши новые данные в качестве атрибута расширения в модель «Стоимость доставки»:
> app/code/MageWorx/ShippingRateByProductAttribute/etc/extension_attributes.xml <?xml version="1.0"?> <!-- /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd"> <!-- Rate Extension --> <extension_attributes for="MageWorx\ShippingRules\Api\Data\RateInterface"> <attribute code="shippingnew" type="int"> <join reference_table="mageworx_shippingrules_rates_shippingnew" reference_field="rate_id" join_on_field="rate_id"> <field>shippingnew</field> </join> </attribute> </extension_attributes> </config>
Мы также должны позаботиться о регулярных операциях нашего пользовательского условия:
> app/code/MageWorx/ShippingRateByProductAttribute/etc/events.xml <?xml version="1.0"?> <!-- /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> <!-- Add Extension Attributes to the Rates Collection --> <!-- Save custom attribute value during rate saving --> <event name="mageworx_shippingrules_rate_save_after"> <observer name="mageworx_save_shippingnew_attribute" instance="MageWorx\ShippingRateByProductAttribute\Observer\SaveShippingNewRateAttribute" /> </event> <!-- Add custom attribute value to the rates collection --> <event name="rates_collection_render_filters_before"> <observer name="mageworx_add_shippingnew_attribute" instance="MageWorx\ShippingRateByProductAttribute\Observer\AddShippingNewToRatesCollection" /> </event> <!-- Take care of filtering the rates grid --> <event name="mageworx_suitable_rates_collection_load_before"> <observer name="mageworx_filter_rates_by_shippingnew_attribute" instance="MageWorx\ShippingRateByProductAttribute\Observer\FilterRatesCollectionByShippingNewAttribute" /> </event> <!-- 3 event observers for the Export/Import rates with custom attribute in conditions --> <event name="mageworx_rates_export_collection_join_linked_tables_after"> <observer name="mageworx_join_shipping_new_table_to_export_rates_collection" instance="MageWorx\ShippingRateByProductAttribute\Observer\JoinShippingNewTableToExportRatesCollection" /> </event> <event name="mageworx_filter_rates_data_before_insert"> <observer name="mageworx_remove_shipping_new_before_insert" instance="MageWorx\ShippingRateByProductAttribute\Observer\RemoveShippingNewBeforeInsert" /> </event> <event name="mageworx_shippingrules_import_insert_rates"> <observer name="mageworx_shippingrules_import_insert_update_shipping_new" instance="MageWorx\ShippingRateByProductAttribute\Observer\InsertUpdateShippingNewDuringImport" /> </event> </config>
Первое событие предназначено для сохранения/обновления/удаления значения пользовательского атрибута в условии ставок.
Вторые два события предназначены для добавления этого значения атрибута в коллекцию.
Последние три события относятся к функциям импорта/экспорта.
Разберем их по порядку более подробно:
> app/code/MageWorx/ShippingRateByProductAttribute/Observer/SaveShippingNewRateAttribute.php <?php /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ namespace MageWorx\ShippingRateByProductAttribute\Observer; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\Exception\LocalizedException; use MageWorx\ShippingRules\Api\Data\RateInterface; /** * Class SaveShippingNewRateAttribute * * Saves custom attribute (`shippingnew`) values after model was saved */ class SaveShippingNewRateAttribute implements ObserverInterface { /** * @var \MageWorx\ShippingRateByProductAttribute\Model\ResourceModel\ShippingNew */ private $resource; /** * @var \Magento\Framework\Message\ManagerInterface */ private $messagesManager; /** * SaveVolumeWeightRateAttribute constructor. * * @param \MageWorx\ShippingRateByProductAttribute\Model\ResourceModel\ShippingNew $resource * @param \Magento\Framework\Message\ManagerInterface $messagesManager */ public function __construct( \MageWorx\ShippingRateByProductAttribute\Model\ResourceModel\ShippingNew $resource, \Magento\Framework\Message\ManagerInterface $messagesManager ) { $this->resource = $resource; $this->messagesManager = $messagesManager; } /** * @param Observer $observer * @return void */ public function execute(Observer $observer) { /** @var RateInterface $model */ $model = $observer->getEvent()->getData('rate'); if (!$model instanceof RateInterface) { return; } $shippingNewValue = $model->getData('shippingnew') !== '' ? $model->getData('shippingnew') : null; if ($shippingNewValue === null) { try { $this->resource->deleteRecord($model->getRateId()); } catch (LocalizedException $deleteException) { $this->messagesManager->addErrorMessage( __('Unable to delete the Shipping Category for the Rate %1', $model->getRateId()) ); } } else { try { $this->resource->insertUpdateRecord($model->getRateId(), $shippingNewValue); } catch (LocalizedException $saveException) { $this->messagesManager->addErrorMessage( __('Unable to save the Shipping Category for the Rate %1', $model->getRateId()) ); } } return; } }
Это так просто. Когда мы сохраняем ставку, мы также должны позаботиться о сохранении значения пользовательского атрибута. Если его значение равно null, просто удалите запись.
> app/code/MageWorx/ShippingRateByProductAttribute/Observer/AddShippingNewToRatesCollection.php <?php /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ namespace MageWorx\ShippingRateByProductAttribute\Observer; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; /** * Class AddShippingNewToRatesCollection * * Adds custom attribute to the rates collection. * It will be used later during quote validation. */ class AddShippingNewToRatesCollection implements ObserverInterface { /** * Join custom table to the rates collection to obtain the `shippingnew` attribute anywhere in the code. * * @param Observer $observer * @return void */ public function execute(Observer $observer) { /** @var \MageWorx\ShippingRules\Model\ResourceModel\Rate\Collection $collection */ $collection = $observer->getEvent()->getData('collection'); if (!$collection instanceof \MageWorx\ShippingRules\Model\ResourceModel\Rate\Collection) { return; } if ($collection->isLoaded()) { return; } $joinTable = $collection->getTable('mageworx_shippingrules_rates_shippingnew'); $collection->getSelect() ->joinLeft( $joinTable, '`main_table`.`rate_id` = `' . $joinTable . '`.`rate_id`', ['shippingnew'] ); } }
Чтобы сделать проверку доступной, когда клиент переходит к оформлению заказа или оценке стоимости доставки, давайте присоединим нашу таблицу с пользовательским атрибутом к таблице обычных ставок.
> app/code/MageWorx/ShippingRateByProductAttribute/Observer/FilterRatesCollectionByShippingNewAttribute.php <?php /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ namespace MageWorx\ShippingRateByProductAttribute\Observer; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; /** * Class FilterRatesCollectionByShippingNewAttribute * * Filter rates collection before we load it by custom attribute: shippingnew. * * For more details * * @see \MageWorx\ShippingRules\Model\Carrier\Artificial::getSuitableRatesAccordingRequest() * */ class FilterRatesCollectionByShippingNewAttribute implements ObserverInterface { /** * @param Observer $observer * @return void */ public function execute(Observer $observer) { /** @var \MageWorx\ShippingRules\Model\ResourceModel\Rate\Collection $collection */ $collection = $observer->getEvent()->getData('rates_collection'); if (!$collection instanceof \MageWorx\ShippingRules\Model\ResourceModel\Rate\Collection) { return; } /** @var \Magento\Quote\Model\Quote\Address\RateRequest $request */ $request = $observer->getEvent()->getData('request'); if (!$request instanceof \Magento\Quote\Model\Quote\Address\RateRequest) { return; } /** @var \Magento\Quote\Model\Quote\Item[] $items */ $items = $request->getAllItems() ?? []; $shippingCategories = []; foreach ($items as $item) { $value = $item->getProduct()->getData('shippingnew'); if ($value !== null) { $shippingCategories[] = $value; } } $shippingCategories = array_unique($shippingCategories); $joinTable = $collection->getTable('mageworx_shippingrules_rates_shippingnew'); $collection->getSelect() ->joinLeft( ['sn' => $joinTable], '`main_table`.`rate_id` = `sn`.`rate_id`', ['shippingnew'] ); $collection->getSelect()->where( "`sn`.`shippingnew` IN (?)", $shippingCategories ); } }
Это самый сложный наблюдатель в нашем стеке. Он предназначен для сбора всех значений атрибутов (`$shippingCategories`) из корзины клиента и добавления значения атрибута в качестве фильтра к регулярному набору тарифов (наша таблица уже присоединена). Для простоты я назвал его «Фильтр». Когда работа будет выполнена, покупатель увидит актуальные тарифы на доставку товаров из текущей корзины.
Еще 3 наблюдателя событий предназначены для добавления и получения пользовательских данных при экспорте и импорте тарифов на доставку. Мы пропускаем его код в сообщении блога, но он будет доступен в репозитории с исходным кодом.
Шаг 3. Пользовательский интерфейс
Пришло время добавить наш атрибут в сетку и в форму тарифов на доставку.
Форма
> app/code/MageWorx/ShippingRateByProductAttribute/view/adminhtml/ui_component/mageworx_shippingrules_rate_form.xml <?xml version="1.0" encoding="UTF-8"?> <!-- /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ --> <form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> <fieldset name="conditions"> <field name="shippingnew"> <argument name="data" xsi:type="array"> <item name="options" xsi:type="object">MageWorx\ShippingRateByProductAttribute\Model\Config\Source\ShippingCategory</item> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Shipping Category</item> <item name="dataType" xsi:type="string">int</item> <item name="formElement" xsi:type="string">select</item> <item name="dataScope" xsi:type="string">shippingnew</item> <item name="source" xsi:type="string">mageworx_shippingrules_rate_form.custom_attributes</item> </item> </argument> </field> </fieldset> </form>
Сетка
> app/code/MageWorx/ShippingRateByProductAttribute/view/adminhtml/ui_component/mageworx_shippingrules_rates_regular_listing.xml <?xml version="1.0"?> <!-- Copyright MageWorx. All rights reserved. See LICENSE.txt for license details. --> <listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Ui/etc/ui_configuration.xsd"> <columns name="mageworx_shippingrules_rates_columns"> <column name="shippingnew"> <argument name="data" xsi:type="array"> <item name="options" xsi:type="object">MageWorx\ShippingRateByProductAttribute\Model\Config\Source\ShippingCategory</item> <item name="config" xsi:type="array"> <item name="filter" xsi:type="string">select</item> <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/select</item> <item name="dataType" xsi:type="string">select</item> <item name="label" xsi:type="string" translate="true">Shipping Category</item> <item name="visible" xsi:type="boolean">true</item> <item name="sortOrder" xsi:type="number">40</item> <item name="editor" xsi:type="string">select</item> </item> </argument> </column> </columns> </listing>
Как видите, в этих файлах мы используем пользовательскую исходную модель. Давайте создадим его. Он загрузит соответствующий атрибут («shippingnew») и предоставит нам все доступные значения.
> app/code/MageWorx/ShippingRateByProductAttribute/Model/Config/Source/ShippingCategory.php <?php /** * Copyright MageWorx. All rights reserved. * See LICENSE.txt for license details. */ namespace MageWorx\ShippingRateByProductAttribute\Model\Config\Source; use Magento\Framework\Exception\LocalizedException; /** * Class ShippingCategory * * Obtain options for specified product attribute */ class ShippingCategory extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource { /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface */ protected $productAttributeRepository; /** * @var \Psr\Log\LoggerInterface */ protected $logger; /** * ShippingCategory constructor. * * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $productAttributeRepository * @param \Psr\Log\LoggerInterface $logger */ public function __construct( \Magento\Catalog\Api\ProductAttributeRepositoryInterface $productAttributeRepository, \Psr\Log\LoggerInterface $logger ) { $this->productAttributeRepository = $productAttributeRepository; $this->logger = $logger; } /** * @inheritDoc */ public function getAllOptions() { if (empty($this->_options)) { try { /** @var \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute */ $attribute = $this->productAttributeRepository->get('shippingnew'); $this->_options = $attribute->usesSource() ? $attribute->getSource()->getAllOptions() : []; } catch (LocalizedException $localizedException) { $this->logger->critical($localizedException->getLogMessage()); } } return $this->_options; } }
Кусок кода довольно прост. Мы используем репозиторий атрибутов для загрузки нашего атрибута, а затем получаем из него все параметры (значения). Имейте в виду, что атрибут с кодом `shippingnew` должен быть создан в панели администратора и должен иметь тип ввода выпадающего списка с предопределенными параметрами (значениями). Вы можете сделать это из меню «Магазины > Атрибуты > Товар». Не забудьте добавить этот атрибут в набор атрибутов, который вы используете для продуктов.
Когда все сделано, нам нужно только включить модуль и запустить `setup:upgrade`. Кэш будет очищен автоматически.
Перейдите в сетку тарифов («Магазины > Тарифы на доставку»), и вы увидите новый столбец:
Это условие будет доступно в форме ставок:
Если мы установим для параметра «Расчет цены нескольких тарифов» значение «Использовать тариф с максимальной ценой» в соответствующем методе доставки, при расчете стоимости доставки будет использоваться тариф с самой высокой ценой.
Вот небольшой пример того, как это работает в формате скриншотов:
- Настройте свои продукты
- Настройте тарифы
- Настроить алгоритм расчета цены (в форме способа доставки)
- Проверьте стоимость доставки для соответствующего метода с выбранными продуктами в корзине (в интерфейсе).
Это далеко не все, на что способен модуль Shipping Suite. Не стесняйтесь играть с настройками, чтобы получить желаемый результат.
Буду рад ответить на любые вопросы! Таким образом, не стесняйтесь оставлять свои комментарии в специальном поле ниже.