So, you want to add a custom field to the sales_order
database table now as an order attribute, eh? This can be a bit confusing to newcomers of Magento 2, because of the need to know multiple advanced concepts.
The answer lies with creating a custom database table, and then linking that table to the desired entity with extension attributes. Let's break this down from step 1 to make this as simple as possible.
The first rule of modifying
The first step of adding a field to a core database table? You don't.
This is because you Never Modify The Core™️, and this not only applies to code, but also to other areas of the platform including the database layer. So what is one to do?
Relationships to core tables
The only natural way is create your own custom database table is to create a relationship between it and the core table.
We will add the vendorname_modulename
prefix to all of our database tables to avoid naming collisions with other tables. Since the table we want to create a relationship with is named sales_order
, we will also suffix it with that name.
Given a vendor name of Acme
and a module name of PoNumber
, the name of our custom table becomes:
Create a custom table
Now that we know the name of the table to create, let's go ahead and create it. The "attribute" we'd like to create will be for PO Numbers, or purchase order numbers, which are used by business customers for their internal purchase tracking needs.
This will be a 1:1 table, meaning that only one record is to be assigned per order. Put another way, every single order should only have one PO Number assigned to it. In order to enforce this 1:1 relation, we must also add a unique constraint to the order_id
column. By doing this, if subsequent rows are added for an order_id
that already exists in another row of the database, it will error out, which will enforce the integrity of our database.
What's up with the long
referenceId
value ofACME_PONUMBER_SALES_ORDER_ORDER_ID_SALES_ORDER_ENTITY_ID
?
ACME_PONUMBER_SALES_ORDER
= current tableORDER_ID
= local column name of related foreign columnSALES_ORDER
= foreign table referenceENTITY_ID
= foreign column to referenceThis standard convention helps us find & debug indexes.
If we then inspect the table after running bin/magento setup:upgrade
, we'll see the table has been successfully created with the proper indexes.
Let's also create some quick placeholder data here to test out our newly found functionality:
Remember to also generate your DB declaration whitelist file by running:
bin/magento setup:db-declaration:generate-whitelist --module-name=Acme_PoNumber
Running this command creates a file next to your
db_schema.xml
file nameddb_schema_whitelist.json
. This JSON file ensures that whenever yourdb_schema.xml
file is changed, the database schema will be dynamically updated along with it wheneverbin/magento setup:upgrade
is executed.
Create a model, resource model & collection
We have now populated the structure of our new database table, but the PHP files need to be setup so we can communicate with it. At the very least, this involves the creation of a model, resource model & collection.
Model
Resource Model
Collection
Now that our data model is complete, let's talk about how we can connect this database table to the core sales_order
table.
About extension attributes
Now that we have created our custom table, we need a way to "link" the data within that table to the core table. This is done through a feature in Magento 2 known as extension attributes.
When we query the Magento API to get order details, we'll get something like this:
All of this info is in a basic structure which we do not want to tamper with, because there may be third-party tools that rely on this schema. That said, notice the last root property named extension_attributes
. This is a place that Magento created for third-parties, including us, to add our own custom properties.
The internal Magento API also works similarly. Since the data will be placed within an extension_attributes
property, rather than calling $order->getPoNumber()
we would call $order->getExtensionAttributes()->getPoNumber()
.
In order to be able to make this work though, we must first add the PO Number data to the Order
model by using a plugin.
Create a plugin
Let's create a plugin, which will allow us to easily add data to queries made to the order repository.
After setting this up, let's make another curl request.
We didn't touch base on how to make CURL requests to the Magento API, but put simply:
Get a token by passing in your username/password as Basic Auth:
POST https://magento.test/rest/V1/integration/admin/token
Then get the order details by passing in the Bearer Token to:
GET https://magento.test/rest/V1/orders/1
The call will succeed, but the po_number
will still not show in the resulting JSON object. Why? This happens because data added to extension attributes must also be defined within an XML file.
Create an extension_attributes.xml file
We're nearing the last step here, and that is to define our newly created attribute inside an extension_attributes.xml
file.
This is quite simple. The value for "for" will be the interface we wish to add data to, in this case OrderInterface
, and then we define the attribute code and type of attribute to add:
Upon adding this file, we will now see the po_number
added to the resulting JSON object after making our API call.
Now that the extension_attributes.xml
has been created, we can also change setData()
to use a magic method if we wish, which would be setPoNumber()
. We don't need to do this, but this is also to show how the extensibility works in Magento.
You will now be able to use the following line in any other custom code as you wish, and the related PO Number will always be returned.
Exceptions?
While I suggested never modifying the core above... there are exceptions. In certain situations, it's perfectly acceptable to add columns to existing database tables when the amount of boilerplate code is disproportionate to the output of what you are trying to do.
For example, in my Customize the Magento 2 Checkout course, I do explain how to add custom fields to the core quote_address
and sales_order_address
database tables.
Is this bad practice? Not necessarily, because in this case, the data stored was simpler scalar data (number, booleans, strings, etc.) and not complex objects. Additionally, creating custom pivot tables for these two database tables would create a ridiculous amount of boilerplate code which would far surpass the output of what we are trying to accomplish.
In any case, be sure to use your best judgement rather than blindly following suggestions for "best practices". No best practice is written in stone, so it's ok to deviate from them when situations present themselves, and when it makes sense to do so.
What next?
Now that you have your custom extension attribute setup, try experimenting with new things. For example, you can add a new column to an admin grid, create a custom admin grid and form which saves the value of this extension attribute, or create a data repository for your data model and update your plugin to use that repository instead.
Want to reference the code in this article? Check it out on GitHub.
If you were having a bit of trouble following along, here are a few resources that may help:
- Explore Magento 2 fundamentals & best practices course (1,000+ students)
- Grow your Magento expertise with all courses & lessons (700+ students)
- Learn visually with blocks of code & inline comments (3,000+ students)