Creating extension attributes

Creating a new extension attribute for an existing entity is usually a case of doing the following:

  1. Using setup scripts to set the attribute, column, or table for persistence
  2. Defining the extension attribute via <VendorName>/<ModuleName>/etc/extension_attributes.xml
  3. Adding an after and/or before plugin to the save, get, and getList methods of an entity repository

Moving forward, we are going to create extension attributes for the order entity, that is, customer_note and merchant_note.

We can imagine customer_note as an attribute that does not persist its value(s) in the sales_order table as order entity does, whereas merchant_note attribute does. This is why we created the sales_order.merchant_note column earlier via the UpgradeData

script.

Let's go ahead and create the <MAGELICIOUS_DIR>/Core/Api/Data/CustomerNoteInterface.php file with the following content:

interface CustomerNoteInterface extends MagentoFrameworkApiExtensibleDataInterface
{
const CREATED_BY = 'created_by';
const NOTE = 'note';

public function setCreatedBy($createdBy);
public function getCreatedBy();
public function setNote($note);
public function getNote();
}

The customer_note attribute is going to be a full-blown object, so we will create an interface for it.

While omitted in the example, be sure to set the doc blocks on each method, otherwise the Magento web API will throw an Each getter must have a doc block error once we hook up the plugin methods.

We will then create the <MAGELICIOUS_DIR>/Core/Model/CustomerNote.php file with the following content:

class CustomerNote extends MagentoFrameworkModelAbstractExtensibleModel implements MageliciousCoreApiDataCustomerNoteInterface
{
public function setCreatedBy($createdBy) {
return $this->setData(self::CREATED_BY, $createdBy);
}

public function getCreatedBy() {
return $this->getData(self::CREATED_BY);
}

public function getNote() {
return $this->getData(self::NOTE);
}

public function setNote($note) {
return $this->setData(self::NOTE, $note);
}
}

This class is essentially our customer_note entity model. To keep things minimal, we will just implement the CustomerNoteInterface, without any extra logic.

We will then go ahead and create the <MAGELICIOUS_DIR>/Core/etc/extension_attributes.xml file with the following content:

<?xml version="1.0"?>

<config>
<extension_attributes for="MagentoSalesApiDataOrderInterface">
<attribute code="customer_note" type="MageliciousCoreApiDataCustomerNoteInterface"/>
<attribute code="merchant_note" type="string"/>
</extension_attributes>
</config>

The extension_attributes.xml file is where we register our extension attributes. The type argument allows us to register either complex types, such as an interface, or scalar types, such as a string or integer. With the extension attributes registered, it is time to register the corresponding plugins. This is done via the di.xml file.

Let's go ahead and create the <MAGELICIOUS_DIR>/Core/etc/di.xml file with the following content:

<?xml version="1.0"?>

<config>
<preference for="MageliciousCoreApiDataCustomerNoteInterface" type="MageliciousCoreModelCustomerNote"/>
<type name="MagentoSalesApiOrderRepositoryInterface">
<plugin name="customerNoteToOrderRepository" type="MageliciousCorePluginCustomerNoteToOrderRepository"/>
<plugin name="merchantNoteToOrderRepository" type="MageliciousCorePluginMerchantNoteToOrderRepository"/>
</type>
</config>

The reason for registering plugins in the first place is to have our customer_note and merchant_note attributes available on the getList, get, and save methods of the MagentoSalesApiOrderRepositoryInterface interface. The repository interfaces are the main way of CRUD-ing entities under service contracts. Without proper plugins, Magento simply would not see our attributes.

Let's create the <MAGELICIOUS_DIR>/Core/Plugin/CustomerNoteToOrderRepository.php file with the following content:

class CustomerNoteToOrderRepository {
protected $orderExtensionFactory;
protected $customerNoteInterfaceFactory;

public function __construct(
MagentoSalesApiDataOrderExtensionFactory $orderExtensionFactory,
MageliciousCoreApiDataCustomerNoteInterfaceFactory $customerNoteInterfaceFactory
) {
$this->orderExtensionFactory = $orderExtensionFactory;
$this->customerNoteInterfaceFactory = $customerNoteInterfaceFactory;
}

private function getCustomerNoteAttribute(
MagentoSalesApiDataOrderInterface $resultOrder
) {
$extensionAttributes = $resultOrder->getExtensionAttributes() ?: $this->orderExtensionFactory->create();

// TODO: Get customer note from somewhere (below we fake it)
$customerNote = $this->customerNoteInterfaceFactory->create()
->setCreatedBy('Mark')
->setNote('The note ' . ime());

$extensionAttributes->setCustomerNote($customerNote);
$resultOrder->setExtensionAttributes($extensionAttributes);
return $resultOrder;
}

private function saveCustomerNoteAttribute(
MagentoSalesApiDataOrderInterface $resultOrder
) {
$extensionAttributes = $resultOrder->getExtensionAttributes();
if ($extensionAttributes && $extensionAttributes->getCustomerNote()) {
// TODO: Save $extensionAttributes->getCustomerNote() somewhere
}
return $resultOrder;
}
}

Right now, there are no plugin methods defined. getCustomerNoteAttribute and saveCustomerNoteAttribute are essentially helper methods that we will soon use.

Let's extend our CustomerNoteToOrderRepository class by adding the after plugin for the getList method, as follows:

public function afterGetList(
MagentoSalesApiOrderRepositoryInterface $subject,
MagentoSalesModelResourceModelOrderCollection $resultOrder
) {
foreach ($resultOrder->getItems() as $order) {
$this->afterGet($subject, $order);
}
return $resultOrder;
}

Now, let's extend our CustomerNoteToOrderRepository class by adding the after plugin for the get method, as follows:

public function afterGet(
MagentoSalesApiOrderRepositoryInterface $subject,
MagentoSalesApiDataOrderInterface $resultOrder
) {
$resultOrder = $this->getCustomerNoteAttribute($resultOrder);
return $resultOrder;
}

Finally, let's extend our CustomerNoteToOrderRepository class by adding the after plugin for the save method, as follows:

public function afterSave(
MagentoSalesApiOrderRepositoryInterface $subject,
MagentoSalesApiDataOrderInterface $resultOrder
) {
$resultOrder = $this->saveCustomerNoteAttribute($resultOrder);
return $resultOrder;
}

With the plugins for customer_note sorted, let's go ahead and address the plugins for merchant_note. We will create the <MAGELICIOUS_DIR>/Core/Plugin/MerchantNoteToOrderRepository.php file with the following content:

class MerchantNoteToOrderRepository {
protected $orderExtensionFactory;

public function __construct(
MagentoSalesApiDataOrderExtensionFactory $orderExtensionFactory
) {
$this->orderExtensionFactory = $orderExtensionFactory;
}

private function getMerchantNoteAttribute(
MagentoSalesApiDataOrderInterface $order
) {
$extensionAttributes = $order->getExtensionAttributes() ?: $this->orderExtensionFactory->create();
$extensionAttributes->setMerchantNote($order->getData('merchant_note'));
$order->setExtensionAttributes($extensionAttributes);
return $order;
}

private function saveMerchantNoteAttribute(
MagentoSalesApiDataOrderInterface $order
) {
$extensionAttributes = $order->getExtensionAttributes();
if ($extensionAttributes && $extensionAttributes->getMerchantNote()) {
$order->setData('merchant_note', $extensionAttributes->getMerchantNote());
}
return $order;
}
}

Right now, there are no plugin methods defined. getMerchantNoteAttribute and saveMerchantNoteAttribute are essentially helper methods that we will soon use.

Let's extend our MerchantNoteToOrderRepository class by adding the after plugin for the getList method, as follows:

public function afterGetList(
MagentoSalesApiOrderRepositoryInterface $subject,
MagentoSalesModelResourceModelOrderCollection $order
) {
foreach ($order->getItems() as $_order) {
$this->afterGet($subject, $_order);
}
return $order;
}

Now, let's extend our MerchantNoteToOrderRepository class by adding the after plugin for the get method, as follows:

public function afterGet(
MagentoSalesApiOrderRepositoryInterface $subject,
MagentoSalesApiDataOrderInterface $order
) {
$order = $this->getMerchantNoteAttribute($order);
return $order;
}

Finally, let's extend our MerchantNoteToOrderRepository class by adding the before plugin for the save method, as follows:

public function beforeSave(
MagentoSalesApiOrderRepositoryInterface $subject,
MagentoSalesApiDataOrderInterface $order
) {
$order = $this->saveMerchantNoteAttribute($order);
return [$order];
}

The obvious difference here is that, with MerchantNoteToOrderRepository, we are using beforeSave, whereas we used afterSave with CustomerNoteToOrderRepository. The reason for this is that merchant_note is to be saved directly on the entity whose repository we are plugging into, that is, its table in the sales_order database. This way, we use its MagentoFrameworkDataObject properties of setData to fetch what was assumingly note already set via extension attributes and pass it onto the object's merchant_note property before it is saved. Magento's built-in save mechanism then takes over and stores the property, as long as the corresponding column exists in the database.

With the plugins in place, our attributes should now be visible and persistable when used through the OrderRepositoryInterface. Without getting too deep into the web API at this point, we can quickly test this via performing the following REST request:

GET /index.php/rest/V1/orders?searchCriteria[filter_groups][0][filters][0][field]=entity_id&searchCriteria[filter_groups][0][filters][0][value]=1
Host: magelicious.loc
Content-Type: application/json
Authorization: Bearer 0vq6d4kabpxgc5kysb2sybf3n4ct771x

Whereas the Bearer token is something we get by running the following REST login action:

POST /index.php/rest/V1/integration/admin/token
Host: magelicious.loc
Content-Type: application/json
{"username": "john", "password": "grdM%0i9a49n"}

The successful response of GET /V1/orders should yield a result of the following partial structure:

{
"items": [
{
"extension_attributes": {
"shipping_assignments": [...],
"customer_note": {
"created_by": "Mark",
"note": "Note ABC"
},
"merchant_note": "Note XYZ"
}
}
]
}
We can see that our two attributes are nicely nested within the extension_attributes key.
Postman, the API development tool, makes it easy to test APIs. See https://www.getpostman.com for more information.

The OrderRepositoryInterface to web API REST relationship maps out as follows:

  • getList: GET /V1/orders (plus the search criteria part)
  • get: GET /V1/orders/:id
  • save: POST /V1/orders/create

We will learn more about the web API in the next chapter. The example given here was merely for the purpose of visualizing the work we have done around plugins. Using extension attributes, with the help of plugins, we have essentially extended the Magento web API.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.130.31