Author: david.mann

IMG_3621

Add an order attribute in Magento 2 – expose in API and show in grid – part 4


In this post, we’ll expose our new attribute in the API so that our finance system can retrieve the value of this attribute when it makes a REST request for an order. There are two steps, first add a getter method for this attribute to the order extension interface and for the class which implements it, and second add the new attribute’s value during order data retrieval. We’ll do the second with a plugin that gets the attribute and sets it in methods which add attributes for both single and multiple records (i.e. for multiple in the case of an order search).

First step, create getter methods:-

DavidMann/OrderSource/etc/​extension_attributes.xml – add getter methods to the order extension interface

1<?xml version=”1.0″?>

2<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:Api/etc/extension_attributes.xsd”>

3<extension_attributes for=”Magento\Sales\Api\Data\OrderInterface”>

4<attribute code=”order_source” type=”string” />

5</extension_attributes>

6</config>

Note we’ve also created setter methods with this step.

To actually generate the getter methods and setter methods, we’ll need to use the compile command at the CLI:

bin/magento setup:di:compile

Only then we’ll see the interface generated/code/Magento/Sales/Api/Data/​OrderExtensionInterface.php and it’s implementation generated/code/Magento/​Sales/Api/Data/OrderExtension.php with added getter and setter methods for our new attribute order_source.

generated/code/Magento/​Sales/Api/Data/​OrderExtension.php

531/**

532* @return string|null

533*/

534public function getOrderSource()

535{

536return $this->_get(‘order_source’);

537}

538

539/**

540* @param string $orderSource

541* @return $this

542*/

543public function setOrderSource($orderSource)

544{

545$this->setData(‘order_source’, $orderSource);

546return $this;

547}

Next step is to register a plugin as shown in orange in our already created di.xml.

DavidMann/OrderSource/etc/​di.xml – register the plugin that will add the new attribute to order data (orange)

1<?xml version=”1.0″?>

2<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:ObjectManager/etc/config.xsd”>

3<virtualType name=”Magento\Sales\Model\ResourceModel\Order\Grid” type=”Magento\Sales\Model\ResourceModel\Grid”>

4<arguments>

5<argument name=”columns”>

6<item name=”order_source” xsi:type=”string”>sales_order.order_source</item>

7</argument>

8</arguments>

9</virtualType>

10<type name=”Magento\Sales\Api\OrderRepositoryInterface”>

11<plugin name=”orderSourceUpdate” type=”DavidMann\OrderSource\Plugin\Api\OrderRepository” />

12</type>

13</config>

And next in the plugin we’ll add the value of order_source to the order for both of our REST requests, for retrieving single and multiple records.

DavidMann/OrderSource/​Plugin/Api/OrderRepository.php – add our custom field value during the order data loading

1<?php

2

3namespace DavidMann\OrderSource\Plugin\Api;

4

5use Magento\Sales\Api\Data\OrderExtensionFactory;

6use Magento\Sales\Api\Data\OrderInterface;

7use Magento\Sales\Api\Data\OrderSearchResultInterface;

8use Magento\Sales\Api\OrderRepositoryInterface;

9

10

11class OrderRepository

12{

13

14const ORDER_SOURCE = ‘order_source’;

15

16/**

17* Order Extension Attributes Factory

18*

19* @var OrderExtensionFactory

20*/

21protected $extensionFactory;

22

23/**

24* OrderRepositoryPlugin constructor

25*

26* @param OrderExtensionFactory $extensionFactory

27*/

28public function __construct(OrderExtensionFactory $extensionFactory)

29{

30$this->extensionFactory = $extensionFactory;

31}

32

33/**

34* Add “order_source” extension attribute to order to make it accessible in API data

35*

36* @param OrderRepositoryInterface $subject

37* @param OrderInterface $order

38*

39* @return OrderInterface

40*/

41public function afterGet(OrderRepositoryInterface $subject, OrderInterface $order)

42{

43$orderSource = $order->getData(self::ORDER_SOURCE);

44$extensionAttributes = $order->getExtensionAttributes();

45$extensionAttributes = $extensionAttributes ? $extensionAttributes : $this->extensionFactory->create();

46$extensionAttributes->setOrderSource($orderSource);

47$order->setExtensionAttributes($extensionAttributes);

48

49return $order;

50}

51

52/**

53* Add “order_source” extension attribute to order to make it accessible in API data

54*

55* @param OrderRepositoryInterface $subject

56* @param OrderSearchResultInterface $searchResult

57*

58* @return OrderSearchResultInterface

59*/

60public function afterGetList(OrderRepositoryInterface $subject, OrderSearchResultInterface $searchResult)

61{

62$orders = $searchResult->getItems();

63

64foreach ($orders as &$order) {

65$orderSource = $order->getData(self::ORDER_SOURCE);

66$extensionAttributes = $order->getExtensionAttributes();

67$extensionAttributes = $extensionAttributes ? $extensionAttributes : $this->extensionFactory->create();

68$extensionAttributes->setOrderSource($orderSource);

69$order->setExtensionAttributes($extensionAttributes);

70}

71

72return $searchResult;

73}

74}

And run bin/magento setup:di:compile again.

Try now retrieving the order data in Postman or your preferred alternative tool.

We’ll need to get a token initially for the session before making either of our requests with:-

https://davidmann.test/rest/V1/integration/admin/token?username=davidmann&password=*********

The request for a single order record is quite straightforward:-

https://davidmann.test/rest/V1/orders/42408

where 42408 is the order ID. As part of the request, we’ll also need to specify the token in ‘Authorization’ as a ‘Bearer Token’ in Postman.

This request uses the afterGet method on line 41 of DavidMann/OrderSource/​Plugin/Api/OrderRepository.php.

You can see order_source and its value for this record in the Postman response to your request:

https://davidmann.test/rest/​V1/orders/42408 – Response under ‘extension_attributes’

57“extension_attributes”: {

58“converting_from_quote”: true,

59“connectors_sales_order”: {

60“parent_id”: 42408,

61“is_exported_to_io”: 0

62},

63“order_source”: “Admin”

64}

65}

The request for multiple records is a little more demanding:-

https://davidmann.test/rest/V1/orders?searchCriteria[filter_groups][0][filters][0][field]=order_source&searchCriteria[filter_groups][0][filters][0][value]=Admin&searchCriteria[filter_groups][0][filters][0][condition_type]=eq
https://magento2.test/rest/V1/orders?
searchCriteria[filter_groups][0][filters][0][field]=order_source
&searchCriteria[filter_groups][0][filters][0][value]=Admin
&searchCriteria[filter_groups][0][filters][0][condition_type]=eq

Again, we’ll need to specify the token in ‘Authorization’ as a ‘Bearer Token’ in Postman.
This request pulls back all orders placed in Admin, i.e. those that have the value ‘Admin’ in the order_source column.

This request uses the afterGetList method on line 60 of DavidMann/OrderSource/​Plugin/Api/OrderRepository.php.

When constructing a search, keep the following in mind:
1) To perform a logical OR, specify multiple filters within a filter_groups.
2) To perform a logical AND, specify multiple filter_groups.

For example, to request records created between April 26, 2020 AND April 30, 2020 specify two filter groups with value 0 and 1.

https://davidmann.test/rest/V1/orders?
searchCriteria[filter_groups][0][filters][0][field]=created_at
&searchCriteria[filter_groups][0][filters][0][value]=2020-04-26T04:00:00.0000000Z
&searchCriteria[filter_groups][0][filters][0][condition_type]=from
&searchCriteria[filter_groups][1][filters][0][field]=created_at
&searchCriteria[filter_groups][1][filters][0][value]=2020-04-30T17:59:00.0000000Z
&searchCriteria[filter_groups][1][filters][0][condition_type]=to
&searchCriteria[currentPage]=1
&searchCriteria[pageSize]=100

That’s it! We’ve successfully exposed order_source to incoming requests by providing the necessary methods and data.

IMG_3620

Add an order attribute in Magento 2 – expose in API and show in grid – part 3


In this part of our series, we want to show order_source in the order grid in Admin. We’re close, the value of order_source has now been stored in the sales_order_grid table, and now we want to see it.

The solution itself is relatively straight forward, just a few lines of well chosen xml:

DavidMann/OrderSource/view/​adminhtml/ui_component/​sales_order_grid.xml – make the new attribute available to the grid column UI component

1<listing xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Ui:etc/ui_configuration.xsd”>

2<columns name=”sales_order_columns”>

3<column name=”order_source”>

4<argument name=”data” xsi:type=”array”>

5<item name=”config” xsi:type=”array”>

6<item name=”filter” xsi:type=”string”>text</item>

7<item name=”label” xsi:type=”string” translate=”true”>Order Source</item>

8</item>

9</argument>

10</column>

11</columns>

12</listing>

This along with other core sales order grid columns declared in vendor/magento/module-sales/view/adminhtml/ui_component/​sales_order_grid.xml is then available ultimately to be displayed in the sales order grid with the UI ‘Listing’ Component and its child UI Component ‘Column’.

We’ll see a little later how the javascript including knockoutjs and the templates of these UI components gets the required values for the grid from a large JSON object partly derived from the xml above.

But before we look at the UI components, I want to make a couple of stops along the way.

As part of rendering the sales order grid page, Magento reads layout xml converting it to layout element objects. You can see that here:-

vendor/magento/framework/​/Model/Layout/​Merge.php

773protected function _loadFileLayoutUpdatesXml()

774{

780foreach ($updateFiles as $file) {

786$fileXml = $this->_loadXmlString($fileStr);

821return $layoutXml;

822}

Where ‘$fileXml = $this->_loadXmlString($fileStr)’ calls this basic PHP function:-

vendor/magento/framework/​View/Model/Layout/​Merge.php

562protected function _loadXmlString($xmlString)

563{

564return simplexml_load_string($xmlString, \Magento\Framework\View\Layout\Element::class);

565}

In the case where $file = “/var/www/html/vendor/​magento/module-sales/view/​adminhtml/layout/​sales_order_index.xml” in the foreach loop on line 780 of vendor/magento/​​framework/View/Model/​​Layout/Merge.php, then the xml content is this:-

vendor/magento/module-sales/​view/adminhtml/layout/​sales_order_index.xml

1<?xml version=”1.0″?>

2<!–

3/**

4* Copyright © Magento, Inc. All rights reserved.

5* See COPYING.txt for license details.

6*/

7–>

8<page xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:View/Layout/etc/page_configuration.xsd”>

9<update handle=”styles”/>

10<body>

11<referenceContainer name=”content”>

12<uiComponent name=”sales_order_grid”/>

13</referenceContainer>

14</body>

15</page>

This layout positions the sales_order_grid UI component on the admin/sales/order/index page. After simplexml_load_string on line 564 of vendor/magento/​framework/View/Model/​Layout/Merge.php, the Layout Element looks like this:-

fileXML

If you want to hit breakpoints in your IDE to examine this yourself, then remember to invoke ‘bin/magento cache:flush layout’ otherwise Magento will grab values from layout cache rather than files. And conditional breakpoints are useful here if you don’t want to cycle through every layout file!

Next comes the generation of layout blocks, and then Magento reads UI Component xml including our custom file DavidMann/OrderSource/​view/​adminhtml/​ui_component/sales_order_grid.xml.

vendor/magento/framework/​View/Layout/Reader/​UiComponent.php

83public function interpret(Context $readerContext, Element $currentElement)

84{

100$config = $this->uiConfigFactory->create([‘componentName’ => $referenceName])->get($referenceName);

111}

This cycles through UI Component xml for the page and when $referenceName = “sales_order_grid” we get an array $config that looks like this:-

config_uicomponent

As well the core columns from vendor/magento/​module-sales/​view/adminhtml/​ui_component/​sales_order_grid.xml, you can see the created array contains the custom column order_source from DavidMann/OrderSource/​view/​adminhtml/ui_component/​sales_order_grid.xml.

Now it gets interesting – the process of converting this array to JSON and making it accessible to UI Components in the sales order grid.

Look at this code:

vendor/magento/module-ui/​TemplateEngine/Xhtml/Result.php

112public function __toString()

113{

124$this->appendLayoutConfiguration();

125$result = $this->compiler->postprocessing($this->template->__toString());

134}

Line 124 calls the method in Result.php – ‘appendLayoutConfiguration()’ which has two jobs. Firstly on lines 101 & 102, it JSON encodes the $config array created by vendor/magento/framework/​View/Layout/Reader/​UiComponent.php already. Recall that $config contains the combined sale order grid columns. Secondly, on line 104 it appends the resultant JSON and other text to the parent html template that displays the sales grid.

vendor/magento/module-ui/​TemplateEngine/Xhtml/​Result.php

99public function appendLayoutConfiguration()

100{

101$layoutConfiguration = $this->wrapContent(

102$this->jsonSerializer->serialize($this->structure->generate($this->component))

103);

104$this->template->append($layoutConfiguration);

105}

The $data input into the serialize method in vendor/magento/framework/​Serialize/Serializer/​JsonHexTag.php is the $config array.

vendor/magento/framework/​Serialize/Serializer/​JsonHexTag.php

27public function serialize($data): string

28{

29$result = json_encode($data, JSON_HEX_TAG);

30if (false === $result) {

31throw new \InvalidArgumentException(‘Unable to serialize value.’);

32}

33return $result;

34}

The output $result is $config as JSON. Here’s a snippet:

vendor/magento/framework/​Serialize/Serializer/​JsonHexTag.php – return $result

1{

2“types”:{

3“dataSource”:[

4

5],

6“bookmark”:{

7“extends”:”sales_order_grid”,

8“current”:{

9“columns”:{

10“increment_id”:{

11“visible”:true,

12“sorting”:false

13},

14“store_id”:{

15“visible”:true,

16“sorting”:false

17},

18“billing_name”:{

19“visible”:true,

20“sorting”:false

21},

22“shipping_name”:{

23“visible”:true,

24“sorting”:false

25},

26“base_grand_total”:{

27“visible”:false,

28“sorting”:false

29},

30“grand_total”:{

31“visible”:true,

32“sorting”:false

33},

34

35

36“created_at”:{

37“sorting”:”desc”,

38“visible”:true

39},

40“order_source”:{

41“sorting”:false,

42“visible”:true

43}

44},

45“displayMode”:”grid”,

All columns including order_source are now in JSON format. Looking back at line 101 in vendor/magento/module-ui/TemplateEngine/Xhtml/Result.php appendLayoutConfiguration(), we see a call to wrapContent after the JSON has been assembled.

vendor/magento/module-ui/​TemplateEngine/Xhtml/​Result.php

142protected function wrapContent($content)

143{

144return ‘<script type=”text/x-magento-init”><![CDATA[‘

145. ‘{“*”: {“Magento_Ui/js/core/app”: ‘ . str_replace([‘<![CDATA[‘, ‘]]>’], ”, $content) . ‘}}’

146. ‘]]></script>’;

147}

This method adds a string containing the script tag, ‘text/x-magento-init’ and ‘{“*”: {“Magento_Ui/js/core/app”:’ to the JSON string Magento has just built. When Magento encounters a ‘text/x-magento-init’ script tag with an ‘*’ attribute, it will

1) Initialize the specified RequireJS module

2) Call the function returned by that module, passing in the data object – which is the JSON object that Magento converted from the array.

But currently this string doesn’t exist in any HTML template so we want to see what happens next and where this string is placed. Line 104 in the appendLayoutConfiguration method of vendor/magento/module-ui/TemplateEngine/​Xhtml/Result.php calls the append method in vendor/magento/framework/​View/TemplateEngine/​Xhtml/Template.php

vendor/magento/framework/​View/TemplateEngine/Xhtml/​Template.php

57public function append($content)

58{

59$ownerDocument= $this->templateNode->ownerDocument;

60$document = new \DOMDocument();

61$document->loadXml($content, LIBXML_PARSEHUGE);

62$this->templateNode->appendChild(

63$ownerDocument->importNode($document->documentElement, true)

64);

65}

The append method selects the template associated with this UI Component and appends the newly created string to the end of it.

This is the template with the appended script tag shown in orange. As we’ve seen the appended script string is created on the fly and is not part of the existing template as you’ll see if you go and take a look at the file in our core Magento code. Only the first part of the script tag contents are shown here due to its size!

vendor/magento/module-ui/view/base/​ui_component/templates/listing/​default.xhtml

8<div

9class=”admin__data-grid-outer-wrap”

10data-bind=”scope: ‘{{getName()}}.{{getName()}}'”

11xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”

12xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Ui:etc/ui_template.xsd”>

13<div data-role=”spinner” data-component=”{{getName()}}.{{getName()}}.{{spinner}}” class=”admin__data-grid-loading-mask”>

14<div class=”spinner”>

15<span/><span/><span/><span/><span/><span/><span/><span/>

16</div>

17</div>

18<!– ko template: getTemplate() –><!– /ko –>

19<script type=”text/x-magento-init”>{“*”: {“Magento_Ui/js/core/app”: {“types”:{“dataSource”:[],”text”:{“component”:…

20</div>

This template presents the entire sales order grid including header information and that includes search, filter and actions. With the JSON object made available by ‘text/x-magento-init’ this and any other HTML templates can access the data needed to populate the sales order grid. There are two templates underneath default.xhtml which pull back data into the grid. The first of these that we want to investigate is vendor/magento/​module-ui/view/base/​web/templates/grid/​listing.html which the knockout binding on line 18 retrieves with:- ‘<!– ko template: getTemplate() –><!– /ko –>’.

vendor/magento/module-ui/​view/base/web/templates/​grid/listing.html

7<div class=”admin__data-grid-wrap” data-role=”grid-wrapper”>

8<table class=”data-grid” data-role=”grid”>

9<thead>

10<tr each=”data: getVisible(), as: ‘$col'” render=”getHeader()”/>

11</thead>

12<tbody>

13<tr class=”data-row” repeat=”foreach: rows, item: ‘$row'” css=”‘_odd-row’: $index % 2″>

14<td outerfasteach=”data: getVisible(), as: ‘$col'”

15css=”getFieldClass($row())” click=”getFieldHandler($row())” template=”getBody()”/>

16</tr>

17<tr ifnot=”hasData()” class=”data-grid-tr-no-data”>

18<td attr=”colspan: countVisible()” translate=”‘We couldn\’t find any records.'”/>

19</tr>

20</tbody>

21</table>

22</div>

This template is responsible for pulling back each row of the grid with the knockoutjs ‘foreach’ loop on line 13 and within that loop, on lines 14 & 15 pulling back each column of the row with the ‘outerfasteach’ loop. Within the inner ‘outerfasteachloop’ loop on line 18, you can see ‘template=”getBody”‘. This is a reference to the knockoutjs binding in listing.js – the javascript component of this Listing UI Component. On line 234 of listing.js , you can see the knockout binding, the binding name ‘getTemplate’ and the binding value which is a function.

vendor/magento/module-ui/view/​base/web/js/grid/​listing.js

234getTemplate: function () {

235var mode = this.displayModes[this.displayMode];

236return mode.template;

237},

This binding pulls back the template name which is vendor/magento/module-ui/​view/base/web/templates/​grid/cells/text.html. Within the template text.html you can see just one line of HTML comprising the knockoutjs getLabel.

vendor/magento/module-ui/​view/base/web/templates/​grid/cells/text.html

7<div class=”data-grid-cell-content” text=”$col.getLabel($row())”/>

And in the javascript for the column UI Component you can see this binding:

vendor/magento/module-ui/view/​base/web/js/grid/​columns/column.js

300getLabel: function (record) {

301return record[this.index];

302},

That takes us to the end of our journey. We’ve skirted over a couple of topics including knockoutjs, but we’ll end it there and provide more detail in a subsequent post.

In the next post, we’ll expose order_source in the API.

IMG_3618

Add an order attribute in Magento 2 – expose in API and show in grid – part 1


The purpose of this series of posts is (1) to provide a solution for the requirement to add an order attribute, expose it in the API and show it in the order grid, and (2) to look in depth at the Magento topics related to the solution.

Topics include dependency injection, Observers, UI components, Knockout JS, the Magento API, generated code, updating the database and Magento areas, e.g. adminhtml and frontend.

(more…)

review_after

Developing Check Out with AJAX – Part 1


This two-part series will look at the development of Magento checkout to meet the requirements of a client using Enterprise 1.13. We won’t deal with the basics of setting up modules or extending classes. I’ve an earlier series on that. Here, I want to focus on checkout’s end-to-end inner workings — a post I’ve wanted to do for some time.

(more…)