Squeezing the extension attributes in Magento 2 #Codehacks
In this post I will explain how to make correct use of the extension attributes. The first thing is to know what this new Magento 2 functionality is for
Extension attributes are used to add new attributes to models without having to modify neither the class nor the interface of them. This applies to both Magento’s own models and third-party modules. The only requirement is that the model interface that we are going to extend inherits from Magento \Framework \ Api \ ExtensibleDataInterface and its Magento \ Framework \ Model\ AbstractExtensibleModel class.
To explain this new functionality, we are going to be based on an own entity, which we explained in the post Creating a new entity in Magento 2.
It should be noted that the entire module follows the standard PHP recommendations.
Creating models, resources and repositories
For the use of the extension attributes we have 2 options, save these new attributes in the same table of the database or in a different one. The example that we are going to explain is adding the new information in a table.
To do this, we will create exactly one new entity with the same steps as in the post Creating a new entity in Magento 2. In our case, in the InstallSchema.php we will create a field that will store the id of the main model which will be a foreign key of the main model field.
app / code / Interactiv4 / CustomPost / Setup / InstallSchema.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | <?php /** * @author Interactiv4 Team * @copyright Copyright © Interactiv4 (https://www.interactiv4.com) */ namespace Interactiv4\CustomPost\Setup; use Interactiv4\CustomPost\Api\Data\PostInterface; use Interactiv4\Post\Api\Data\EntityInterface; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Ddl\Table; use Magento\Framework\Setup\InstallSchemaInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\SchemaSetupInterface; use Zend_Db_Exception; /** * Class InstallSchema */ class InstallSchema implements InstallSchemaInterface { /** * {@inheritdoc} */ public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) { $installer = $setup; $installer->startSetup(); //Custom post $installer->getConnection()->dropTable($installer->getTable(PostInterface::SCHEMA_TABLE)); $this->installTableCustomPost($installer); $installer->endSetup(); } /** * Create table relations between custom entity and custom post * * @param SchemaSetupInterface $installer * @throws Zend_Db_Exception */ private function installTableCustomPost(SchemaSetupInterface $installer) { $table = $installer->getConnection()->newTable( $installer->getTable(PostInterface::SCHEMA_TABLE) )->addColumn( PostInterface::FIELD_ID, Table::TYPE_INTEGER, null, ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], 'Custom post Id' )->addColumn( PostInterface::FIELD_POST_ID, Table::TYPE_INTEGER, null, ['unsigned' => true, 'nullable' => false], 'Post Id' )->addColumn( PostInterface::FIELD_SHORT_DESCRIPTION, Table::TYPE_TEXT, null, ['nullable' => false, 'default' => ''], 'Short description' )->addIndex( $installer->getIdxName( $installer->getTable(PostInterface::SCHEMA_TABLE), [PostInterface::FIELD_POST_ID], AdapterInterface::INDEX_TYPE_UNIQUE ), [PostInterface::FIELD_POST_ID], ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] )->addForeignKey( $installer->getFkName( PostInterface::SCHEMA_TABLE, PostInterface::FIELD_POST_ID, EntityInterface::TABLE, EntityInterface::ID ), PostInterface::FIELD_POST_ID, $installer->getTable(EntityInterface::TABLE), EntityInterface::ID, Table::ACTION_CASCADE )->setComment( 'Custom Post' ); $installer->getConnection()->createTable($table); } } |
Creation of extension attributes
To add the extension attributes to an already created model, we must first create a file extension_attributes.xml, in which we will specify the interface of the main model and the fields that we are going to add.
app / code / Interactiv4 / CustomPost / etc / extension_attributes.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?xml version="1.0"?> <!-- ~ @author Interactiv4 Team ~ @copyright Copyright © Interactiv4 (https://www.interactiv4.com) --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd"> <extension_attributes for="Interactiv4\Post\Api\Data\EntityInterface"> <attribute code="short_description" type="string"> <resources> <resource ref="Interactiv4_Post::entity"/> </resources> <join reference_table="interactiv4_custompost_post" reference_field="post_id" join_on_field="entity_id"> <field>short_description</field> </join> </attribute> </extension_attributes> </config> |
In the example above we can see, as we indicated the interface of the main model, the name of the new attribute and its typology (int, string, an object, array …).
The resources parameter is optional and restricts access to the user with a specific permission.
Note the specification of the join, which is optional. At this point, we specify our new table and the fields that are referenced between the main model and ours. This will only work if the joinProcessor of the extension attributes (Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface) is included in the getList method of the main repository.
app / code / Interactiv4 / Post / Model / EntityRepository.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <?php /** * @author Interactiv4 Team * @copyright Copyright (c) 2017 Interactiv4 (https://www.interactiv4.com) * @package Interactiv4_Post */ namespace Interactiv4\Post\Model; ... use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; class EntityRepository implements EntityRepositoryInterface { ... /** * EntityRepository constructor. * * @param EntityFactory $entityFactory * @param CollectionFactory $entityCollectionFactory * @param EntitySearchResultsInterfaceFactory $entitySearchResultsInterfaceFactory * @param CollectionProcessorInterface $collectionProcessor * @param JoinProcessorInterface $extensionAttributesJoinProcessor */ public function __construct( EntityFactory $entityFactory, CollectionFactory $entityCollectionFactory, EntitySearchResultsInterfaceFactory $entitySearchResultsInterfaceFactory, CollectionProcessorInterface $collectionProcessor, JoinProcessorInterface $extensionAttributesJoinProcessor ) { $this->entityFactory = $entityFactory; $this->entityCollectionFactory = $entityCollectionFactory; $this->entitySearchResultsInterfaceFactory = $entitySearchResultsInterfaceFactory; $this->collectionProcessor = $collectionProcessor; $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; } ... /** * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { /** @var Collection $collection */ $collection = $this->entityCollectionFactory->create(); $this->extensionAttributesJoinProcessor->process($collection, EntityInterface::class); $this->collectionProcessor->process($searchCriteria, $collection); /** @var EntitySearchResultsInterface $searchResults */ $searchResults = $this->entitySearchResultsInterfaceFactory->create(); $searchResults->setSearchCriteria($searchCriteria); $searchResults->setItems($collection->getItems()); $searchResults->setTotalCount($collection->getSize()); return $searchResults; } } |
Thanks to this joinProcessor we can define in our PostRepository.php a function getListPostByShortDescription () which obtains a list of all the entities of the main model filtering by the value of an extension attributes.
app / code / Interactiv4 / CustomPost / Model / PostRepository.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | <?php /** * @author Interactiv4 Team * @copyright Copyright © Interactiv4 (https://www.interactiv4.com) */ namespace Interactiv4\CustomPost\Model; use Exception; use Interactiv4\CustomPost\Api\Data\PostInterface; use Interactiv4\CustomPost\Api\Data\PostSearchResultsInterface; use Interactiv4\CustomPost\Api\Data\PostSearchResultsInterfaceFactory; use Interactiv4\CustomPost\Api\PostRepositoryInterface; use Interactiv4\CustomPost\Model\PostFactory; use Interactiv4\CustomPost\Model\ResourceModel\Post as ResourceCustomPost; use Interactiv4\CustomPost\Model\ResourceModel\Post\Collection; use Interactiv4\CustomPost\Model\ResourceModel\Post\CollectionFactory; use Interactiv4\Post\Api\Data\EntityInterface; use Interactiv4\Post\Api\EntityRepositoryInterface; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\Search\SearchCriteria; use Magento\Framework\Api\Search\SearchCriteriaBuilder; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SortOrder; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Framework\Exception\ValidatorException; /** * Class PostRepository * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PostRepository implements PostRepositoryInterface { /** * @var ResourceCustomPost */ private $resourceCustomPost; /** * @var PostFactory */ private $customPostFactory; /** * @var CollectionFactory */ private $customPostCollectionFactory; /** * @var PostSearchResultsInterfaceFactory */ private $customPostSearchResultInterfaceFactory; /** * @var CollectionProcessorInterface */ private $collectionProcessor; /** * @var JoinProcessorInterface */ private $extensionAttributesJoinProcessor; /** * @var EntityRepositoryInterface */ private $entityRepository; /** * @var FilterBuilder */ private $filterBuilder; /** * @var SearchCriteriaBuilder */ private $searchCriteriaBuilder; /** * @var SortOrder */ private $sortOrder; /** * PostRepository constructor. * * @param ResourceCustomPost $resourceCustomPost * @param PostFactory $customPostFactory * @param CollectionFactory $customPostCollectionFactory * @param PostSearchResultsInterfaceFactory $customPostSearchResultInterfaceFactory * @param CollectionProcessorInterface $collectionProcessor * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param EntityRepositoryInterface $entityRepository * @param FilterBuilder $filterBuilder * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param SortOrder $sortOrder * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ResourceCustomPost $resourceCustomPost, PostFactory $customPostFactory, CollectionFactory $customPostCollectionFactory, PostSearchResultsInterfaceFactory $customPostSearchResultInterfaceFactory, CollectionProcessorInterface $collectionProcessor, JoinProcessorInterface $extensionAttributesJoinProcessor, EntityRepositoryInterface $entityRepository, FilterBuilder $filterBuilder, SearchCriteriaBuilder $searchCriteriaBuilder, SortOrder $sortOrder ) { $this->resourceCustomPost = $resourceCustomPost; $this->customPostFactory = $customPostFactory; $this->customPostCollectionFactory = $customPostCollectionFactory; $this->customPostSearchResultInterfaceFactory = $customPostSearchResultInterfaceFactory; $this->collectionProcessor = $collectionProcessor; $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; $this->entityRepository = $entityRepository; $this->filterBuilder = $filterBuilder; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->sortOrder = $sortOrder; } /** * @inheritdoc */ public function save(PostInterface $customPost) { $this->resourceCustomPost->save($customPost); return $customPost; } /** * @inheritdoc */ public function getById($customPostId) { return $this->get($customPostId); } /** * @inheritdoc */ public function get($value, $attributeCode = null) { /** @var Post $customPost */ $customPost = $this->customPostFactory->create()->load($value, $attributeCode); if (!$customPost->getId()) { throw new NoSuchEntityException(__('Unable to find custom post')); } return $customPost; } /** * @inheritdoc */ public function delete(PostInterface $customPost) { $customPostId = $customPost->getId(); try { $this->resourceCustomPost->delete($customPost); } catch (Exception $e) { throw new StateException( __('Unable to remove custom post %1', $customPostId) ); } return true; } /** * @inheritdoc */ public function deleteById($customPostId) { $customPost = $this->getById($customPostId); return $this->delete($customPost); } /** * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { /** @var Collection $collection */ $collection = $this->customPostCollectionFactory->create(); $this->extensionAttributesJoinProcessor->process($collection, PostInterface::class); $this->collectionProcessor->process($searchCriteria, $collection); /** @var PostSearchResultsInterface $searchResults */ $searchResults = $this->customPostSearchResultInterfaceFactory->create(); $searchResults->setSearchCriteria($searchCriteria); $searchResults->setItems($collection->getItems()); $searchResults->setTotalCount($collection->getSize()); return $searchResults; } /** * @inheritdoc */ public function getListPostByShortDescription($shortDescription) { $filter = $this->filterBuilder ->setField(PostInterface::FIELD_SHORT_DESCRIPTION) ->setValue($shortDescription) ->setConditionType('LIKE') ->create(); /** @var SearchCriteria $searchCriteria */ $searchCriteria = $this->searchCriteriaBuilder->addFilter($filter); $sortOrder = $this->sortOrder->setField(EntityInterface::NAME)->setDirection('asc'); $searchCriteria->setSortOrders([$sortOrder]); $entitySearchResults = $this->entityRepository->getList($searchCriteria); return $entitySearchResults; } } |
Thanks to the definition of the file extension_attributes.xml, Magento will generate in the folder var / generation (<Magento 2.2) or generated (> Magento 2.2) an EntityExtensionInterface.php class with the getters and setters of the attributes that we have defined in said xml file .
var / generation /Interactiv4 / Post / Api / Data / EntityExtensionInterface.php or
generated / code / Interactiv4 / Post / Api / Data / EntityExtensionInterface.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php /** * @author Interactiv4 Team * @copyright Copyright © Interactiv4 (https://www.interactiv4.com) */ namespace Interactiv4\Post\Api\Data; /** * ExtensionInterface class for @see \Interactiv4\Post\Api\Data\EntityInterface */ interface EntityExtensionInterface extends \Magento\Framework\Api\ExtensionAttributesInterface { /** * @return string|null */ public function getShortDescription(); /** * @param string $shortDescription * @return $this */ public function setShortDescription($shortDescription); } |
Obtain and store extension attribute information
Plugins
Magento does not store or obtain the information of the extension attributes automatically. We have to obtain it and save it manually by using the plugins in the get (), getList () and save () functions of the main repository.
app / etc / di.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?xml version="1.0" encoding="UTF-8"?> <!-- ~ @author Interactiv4 Team ~ @copyright Copyright © Interactiv4 (https://www.interactiv4.com) --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Interactiv4\CustomPost\Api\Data\PostInterface" type="Interactiv4\CustomPost\Model\Post" /> <preference for="Interactiv4\CustomPost\Api\Data\PostSearchResultsInterface" type="Interactiv4\CustomPost\Model\PostSearchResults" /> <preference for="Interactiv4\CustomPost\Api\PostRepositoryInterface" type="Interactiv4\CustomPost\Model\PostRepository" /> <type name="Interactiv4\Post\Api\EntityRepositoryInterface"> <plugin name="interactiv4_custompost_plugin_entity_repository" type="Interactiv4\CustomPost\Plugin\EntityRepositoryPlugin"/> </type> </config> |
app / code / Plugin / EntityRepositoryPlugin.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | <?php /** * @author Interactiv4 Team * @copyright Copyright © Interactiv4 (https://www.interactiv4.com) */ namespace Interactiv4\CustomPost\Plugin; use Interactiv4\CustomPost\Api\Data\PostInterface; use Interactiv4\CustomPost\Api\Data\PostInterfaceFactory; use Interactiv4\CustomPost\Api\PostRepositoryInterface; use Interactiv4\Post\Api\Data\EntityExtensionFactory; use Interactiv4\Post\Api\Data\EntityExtensionInterface; use Interactiv4\Post\Api\Data\EntityInterface; use Interactiv4\Post\Api\Data\EntitySearchResultsInterface; use Interactiv4\Post\Api\EntityRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; /** * Class EntityRepositoryPlugin */ class EntityRepositoryPlugin { /** * @var EntityExtensionFactory */ private $entityExtensionFactory; /** * @var PostInterfaceFactory */ private $customPostFactory; /** * @var PostRepositoryInterface */ private $customPostRepository; /** * PostGet constructor. * * @param EntityExtensionFactory $entityExtensionFactory * @param PostInterfaceFactory $customPostFactory * @param PostRepositoryInterface $customPostRepository */ public function __construct( EntityExtensionFactory $entityExtensionFactory, PostInterfaceFactory $customPostFactory, PostRepositoryInterface $customPostRepository ) { $this->entityExtensionFactory = $entityExtensionFactory; $this->customPostFactory = $customPostFactory; $this->customPostRepository = $customPostRepository; } /** * @param EntityRepositoryInterface $subject * @param EntityInterface $result * @return EntityInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterGet(EntityRepositoryInterface $subject, EntityInterface $result) { /** @var EntityExtensionInterface $extensionAttributes */ $extensionAttributes = $result->getExtensionAttributes() ?: $this->entityExtensionFactory->create(); try { /** @var PostInterface $customPost */ $customPost = $this->customPostRepository->get($result->getId(), PostInterface::FIELD_POST_ID); } catch (NoSuchEntityException $e) { $result->setExtensionAttributes($extensionAttributes); return $result; } $extensionAttributes->setShortDescription($customPost->getShortDescription()); $result->setExtensionAttributes($extensionAttributes); return $result; } /** * @param EntityRepositoryInterface $subject * @param EntitySearchResultsInterface $entities * @return EntitySearchResultsInterface */ public function afterGetList(EntityRepositoryInterface $subject, EntitySearchResultsInterface $entities) { foreach ($entities->getItems() as $entity) { $this->afterGet($subject, $entity); } return $entities; } /** * @param EntityRepositoryInterface $subject * @param EntityInterface $result * @return EntityInterface * @throws CouldNotSaveException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterSave(EntityRepositoryInterface $subject, EntityInterface $result) { $extensionAttributes = $result->getExtensionAttributes() ?: $this->entityExtensionFactory->create(); if ($extensionAttributes !== null && $extensionAttributes->getShortDescription() !== null ) { /** @var PostInterface $customPost */ try { $customPost = $this->customPostRepository->get($result->getId(), PostInterface::FIELD_POST_ID); } catch (NoSuchEntityException $e) { $customPost = $this->customPostFactory->create(); } $customPost->setPostId($result->getId()); $customPost->setShortDescription($extensionAttributes->getShortDescription()); $this->customPostRepository->save($customPost); } return $result; } } |
In this way we get that every time a model or a list is loaded, it contains the additional information of the extension attributes. In turn, each time we store the information of a product, we will also save the additional information.
I hope to have been helpful and if any questions you can leave a comment.
In this link you can download the code of this explanation and in this link the main entity from which we are going to extend.
Share
Subscribe to our newsletter
You may also like
We use third party cookies to improve our services and obtain statistical data of your browsing habits. If you continue browsing we consider that you accept its use. You can get more information at Privacy policy and cookies