Share

Squeezing the extension attributes in Magento 2 #Codehacks

13 March, 2018

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
<?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;

/**
* 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
*/

class PostRepository implements PostRepositoryInterface
{
   /**
    * @var ResourceCustomPost $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
    */

   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 (ValidatorException $e) {
           throw new CouldNotSaveException(__($e->getMessage()));
       } 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
124
125
126
127
<?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
    */

   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
    */

   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.

Category
Tags
Author

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