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

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.

Let me first declare I’m using Magento Open Source 2.4.2, developing in a Docker container with PhpStorm, MySQL Workbench and Postman.

The solution will provide a new order attribute to show where the order originated, the area it originated from. We’ll call this attribute order_source. Initially we’ll only consider orders originating in Admin, keyed by an Administrator, or in Frontend, entered by a customer in checkout. We’ll display this flag, ‘Admin’ or ‘Customer’, in the Order Grid. We’ll expose this new attribute to the API so that it can be picked up by the client’s finance system which makes REST requests. That’s it.

This is how the new column order_source will appear in the order grid:-

Screen Shot 2021-09-22 at 10.59.30 AM

I realize that many visitors to this post will just want a solution whereas others will be interested in better understanding the topics related to this solution.

For the former group, I’ll provide the solution in full in parts 1, 2, 3 & 4 of this series with solution code shown in green. In this post we’ll add the order_source attribute to the sales_order and sales_order_grid tables and save the value of order_source to the order when an order is placed. In part 2 we’ll add the value of order_source to the order grid table. In part 3, we’ll show order_source in the order grid, and in part 4 we’ll expose the attribute and its value in the API when orders are requested.

This and other posts in this series will explore the related topics in depth and look at alternative approaches. If any or all of this is confusing at this stage, bear in mind this sequence of posts aims to provide some clarity.

The next two files register the module in in PHP and in XML.

DavidMann/OrderSource/​registration.php – register the module at PHP level

1<?php

2\Magento\Framework\Component\ComponentRegistrar::register(

3\Magento\Framework\Component\ComponentRegistrar::MODULE,

4‘DavidMann_OrderSource’,

5__DIR__

6);

DavidMann/OrderSource/etc/​module.xml – register the module at configuration level

1<?xml version=”1.0″?>

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

3<module name=”DavidMann_OrderSource” setup_version=”0.0.1″></module>

4</config>

Next we’ll add the custom attribute order_source to the sales_order and sales_order_grid tables:-

DavidMann/OrderSource/etc/​db_schema.xml – add columns to the sales_order & sales_order_grid tables

1<?xml version=”1.0″?>

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

3<table name=”sales_order” engine=”innodb”>

4<column name=”order_source” xsi:type=”text” nullable=”false” comment=”Order Source”/>

5</table>

6<table name=”sales_order_grid” engine=”innodb”>

7<column name=”order_source” xsi:type=”text” nullable=”false” comment=”Order Source”/>

8</table>

9</schema>

It’s worth noting that this file and the next one are only used in Magento versions 2.3 onwards which use the declarative schema to make changes to the database. At the end of this post I’ll show how earlier Magento versions approached this.

DavidMann/OrderSource/etc/​db_schema_whitelist.json – for backward compatibility with install & upgrade schema scripts

1{

2“sales_order”: {

3“column”: {

4“order_source”: true

5}

6},

7“sales_order_grid”: {

8“column”: {

9“order_source”: true

10}

11}

12}

That script can be entered manually, or easier use this command in the CLI to create it for just this module:
bin/magento setup:db-declaration:generate-whitelist –module-name=DavidMann_OrderSource

To update the database with these new table columns you’ll need to run:-
bin/magento setup:upgrade

It’s worth noting that Magento’s new declarative approach prevents changes from being applied more than once – Magento determines the differences between the current table structure and what it should be as specified in the schema files. Maintaining module version numbers in the setup_module table as required by earlier Magento versions is not required.

Declarative mode also provides for dry runs and the roll back of database changes. You can read more about these options here:- https://devdocs.magento.com/guides/v2.4/extension-dev-guide/declarative-schema/db-schema.html.

Next we’ll tap into Magento’s event dispatch system to set the correct value, ‘Admin’ or ‘Customer’, in the order object just before the order is placed.

In vendor/magento/module-sales/Model/Order.php the method place() dispatches the event ‘sales_order_place_before’ with the order object:-

$this->_eventManager->dispatch(‘sales_order_place_before’, [‘order’ => $this]).

This event is dispatched for orders placed in Admin and frontend checkout.

DavidMann/OrderSource/etc/​events.xml – the event will trigger our observer code

1<?xml version=”1.0″?>

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

3<event name=”sales_order_place_before”>

4<observer name=”save_source_to_order” instance=”DavidMann\OrderSource\Observer\SaveSourceToOrder” />

5</event>

6</config>

DavidMann/OrderSource/Observer/​SaveSourceToOrder.php – observer logic adds the right area flag to the order

1<?php

2

3namespace DavidMann\OrderSource\Observer;

4

5use Magento\Framework\Event\ObserverInterface;

6use Magento\Framework\App\State;

7

8class SaveSourceToOrder implements ObserverInterface

9{

10/**

11* Possible order grid order area (source) flag values

12*/

13const ADMIN = ‘Admin’;

14

15const FRONTEND = ‘Customer’;

16

17/**

18* @var State

19*/

20protected $_state;

21

22/**

23* SaveSourceToOrder constructor

24*

25* @param State $state

26*/

27public function __construct(

28State $state

29) {

30$this->_state = $state;

31}

32

33/**

34* Add either Admin or Customer flag to Order based on area (source) of order

35*

36* @param (\Magento\Framework\Event\Observer $observer

37* @return $this

38*/

39public function execute(\Magento\Framework\Event\Observer $observer)

40{

41$order = $observer->getEvent()->getData(‘order’);

42if ($this->_state->getAreaCode() == “adminhtml”) {

43$order->setData(‘order_source’,self::ADMIN);

44} else {

45$order->setData(‘order_source’,self::FRONTEND);

46}

47return $this;

48}

49}

Note Magento\Framework\App\State is injected as a dependency in the constructor. Dependency injection is a fundamental design pattern in Magento 2. We’ll talk more about this later.

Magento\Framework\App\State enables us to get the area from which the request originates, i.e. frontend, adminhtml, graphql, crontab, webapi_rest or webapi_soap.

However, as an alternative approach, we could have Magento process just the observers relating to all the event.xml files selected for a particular area. When an HTTP request is made, Magento merges and processes just the xml relating to a particular area. It gets the area from the url string, e.g in the case of this request its:-

/admin/sales/order/index/​order_id/42416/key/​18241523aaee7f10c1c2274392​6759f22245acabc1e1ef04506b41f875d4f26a/

It finds ‘admin’ in the string and selects only xml stored in directories named adminhtml, e.g. DavidMann/OrderSource/etc/adminhtml/​events.xml. You can see this in vendor/magento/framework/App/Http.php line number 111, where the area depends on a call to getFrontName():-

$areaCode = $this->_areaList->getCodeByFrontName($this->_request->getFrontName())

And getFrontName() explodes the url parts and selects admin from the first item in the consequent array.

vendor/magento/framework/​App/Http.php

109public function launch()

110{

111$areaCode = $this->_areaList->getCodeByFrontName($this->_request->getFrontName());

112$this->_state->setAreaCode($areaCode);

113$this->_objectManager->configure($this->_configLoader->load($areaCode));

The area is included in urls for other areas too, for example in the REST endpoint /rest/V1/orders/42408. This endpoint would select event.xml from ‘webapi_rest’ subdirectories, including, if it existed, DavidMann/OrderSource/etc/webapi_rest/​events.xml for example.

You may be thinking what about frontend requests. The area ‘frontend’ isn’t included in urls for those requests, e.g. /checkout/#shipping. In the case of frontend requests Magento can’t find an area string in the url so it uses the default ‘frontend’ with this ‘return $this->_defaultAreaCode’ in vendor/magento/framework/App/AreaList.php. There is an anomaly associated with the frontend area for order placement and we’ll pick up in a subsequent post.

And you can see in vendor/magento/framework/App/Http.php line 113 that the xml configuration is loaded only for the area code derived in line 111.

If we’d used this approach in our solution and let Magento split our event.xml by area, rather than use an observer requiring injection of ‘Magento\Framework\App\State’, our logic would be easier. In the case of the ‘Admin’ area for example with this line in DavidMann/OrderSource/etc/​adminhtml/events.xml (and note the adminhtml directory):-

<observer name=”save_source_to_admin_order” instance=”DavidMann\OrderSource\Observer\​SaveSourceToAdminOrder” />

our logic could have been this:-

app/code/DavidMann/OrderSource/​Observer/SaveSourceToAdminOrder.php

39public function execute(\Magento\Framework\Event\Observer $observer)

40{

41$order = $observer->getEvent()->getData(‘order’);

42$order->setData(‘order_source’,self::ADMIN);

43

44return $this;

45}

Our discussion to this point will place order_source in the sales_order table when the order is placed, but we still need to add order_source to the sales_order_grid table when the order is placed. We’ll look at that in the next post. And in part 3, we’ll display order_source in the order grid.

However before we finish, we used the declarative schema earlier to update the database, but here’s how we’d update the database prior to version 2.3:

app/code/DavidMann/OrderSource/​Setup/InstallSchema.php

1<?php

2

3namespace DavidMann\OrderSource\Setup;

4use Magento\Framework\DB\Ddl\Table;

5use Magento\Framework\Setup\InstallSchemaInterface;

6use Magento\Framework\Setup\ModuleContextInterface;

7use Magento\Framework\Setup\SchemaSetupInterface;

8

9

10class InstallSchema implements InstallSchemaInterface

11{

12

13/**

14* {@inheritdoc}

15*/

16public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)

17{

18$installer = $setup;

19$installer->startSetup();

20

21$installer->getConnection()->addColumn(

22$installer->getTable(‘sales_order’),

23‘order_source’,

24[

25‘type’ => Table::TYPE_TEXT,

26‘nullable’ => true,

27‘comment’ => ‘Order Source’,

28]

29);

30

31$installer->getConnection()->addColumn(

32$installer->getTable(‘sales_order_grid’),

33‘order_source’,

34[

35‘type’ => Table::TYPE_TEXT,

36‘nullable’ => true,

37‘comment’ => ‘Order Source’,

38]

39);

40

41

42$setup->endSetup();

43}

44}

Again we’ll need to update the database by running this install code with these new table columns by running:-
bin/magento setup:upgrade

An entry is made in the setup_module table showing the version number of the module specified in DavidMann/OrderSource/etc/​module.xml. Magento checks against this version number when bin/magento setup:upgrade is run again to prevent an attempt to add the columns again.


david.mann

Leave a Reply

Your email address will not be published. Required fields are marked *