Professional Developer Final
Professional Developer Final
You made a great decision by purchasing the most comprehensive study guide
for the Magento 2 Certified Professional Developer Plus certification (testing
your knowledge of Magento Commerce 2.2)!
2. For those who are experienced and are ready to go for this certification,
this will be both a refresher on subjects you haven’t worked on lately and
will point out some new areas that you haven’t utilized yet (message
queueing anyone?).
The study guide answers the questions presented in Magento’s study guide
for the Professional Developer Plus test. Just reading this study guide is part of
solid preparation for passing the test but does not guarantee a passing grade.
That is why we have included a "Practical Experience" section on most points.
This will guide you to specific launching points where you can dive in and learn.
Requirements
Please understand that the code examples are designed to work with Magento
Commerce 2.2. If you do not have access to Magento Commerce code, you
will run into parts of the code that do not function properly. In addition, you
might pass the test but will have a very difficult time with the staging and
Commerce questions.
Acknowledgments
I would like to extend heartfelt gratitude to the following individuals who
contributed by edits or comments to the development of the study guide.
These people graciously donated their time to help others out. If you see them
at a conference or on Twitter, please thank them.
• Artem Klimov
• Itonomy Commerce
• Kevin Bodwell
• Евген Швец
• Nadezhda Glonyagina
Joseph Maxwell
ADDITIONAL RESOURCES
Throughout this guide, I will reference many external websites and resources.
Here are a few of my favorites:
• IDE: PHPStorm (by JetBrains). I use this every day that I am writing
Magento code.
This study guide is one of the most (or the most) concentrated source of
information for advanced backend Magento development. If you hope to
achieve this certification or just want to learn, you won’t be disappointed.
Our goal is not to just tell you how to write code, but also point you to where
you can dive in yourself AND show you how to do it. With this three-pronged
approach, your willingness to learn is the only thing stopping you from hitting
your goals.
Magento code
As stated above, you need to have license keys for Magento Commerce
(formerly, Enterprise). If you work for a partner or are a Community Insider, you
will have these keys available to you.
The test
This Professional Developer Plus test consists of 60 questions, requires a 62%
score to pass, and has a time limit of 90 minutes. The questions are scenario-
based, which ensures you have proper knowledge of the subject (instead of
simply memorizing many subjects).
The test ensures you are extremely knowledgeable about Magento 2.2.
CONTENTS
1 MAGENTO ARCHITECTURE........................................... 11
1.1 Determine advanced uses of the Magento
configuration system..................................................... 12
1.2 Demonstrate an ability to design
complex customizations using plugins and di.xml........... 17
1.3 Demonstrate understanding of
Magento events processing........................................... 33
1.4 Demonstrate an ability to
use the Magento command-line interface....................... 35
2 MAGENTO UI................................................................ 39
2.1 Demonstrate understanding UiComponents architecture...... 40
2.2 Demonstrate advanced use of Magento layouts.............. 53
2.3 Demonstrate an ability to
operate with Magento blocks and templates................... 57
APPENDIX.................................................................... 247
Discussion of the Entity Manager................................... 248
Magento includes a number of XML files that cover the majority of use cases
in normal development. Module configuration is located in the app/code/
MyCompany/MyModule/etc/ directory.
Example:
• app/code/Chapter1/ConfigLoadOrder and app/code/Chapter1/
ConfigLoadOrder2
• https://lc.commerce.site/chapter11/
Notice app/etc/config.php:
<?php
return [
'modules' => [
'Magento_Store' => 1,
'Magento_Directory' => 1,
'Magento_Eav' => 1,
'Magento_Customer' => 1,
'Chapter1_ConfigLoadOrder2' => 1,
'Chapter1_ConfigLoadOrder' => 1,
Information:
• Create or extend configuration types
• A class for exposing the configuration data. While this isn’t 100% necessary,
it makes easier maintainability in retrieving the data.
Example:
• app/code/Chapter1/CustomConfig
• https://lc.commerce.site/chapter11custom
Note that for the sake of simplicity, we are using virtualTypes as defined
in di.xml. Also note that virtualTypes do not generate any classes in the
generated/ directory.
Practice:
• Create a new configuration type. Do not copy any code examples in this
section but rather hand type. Pay special attention to the relationship
between classes.
Examples:
• vendor/magento/module-email/Model/Template/Config.php
• vendor/magento/module-email/Model/Template/Config/Data.php
• vendor/magento/module-email/Model/Template/Config/Reader.php
• vendor/magento/module-email/Model/Template/Config/Converter.php
• vendor/magento/module-email/Model/Template/Config/FileResolver.php
Information:
• Create or extend configuration types
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://raw.githubusercontent.com/
magento/magento2/2.3-develop/lib/internal/Magento/Framework/App/
etc/routes.xsd">
<router id="standard">
</route>
</router>
</config>
Note that Magento may throw an error if an https URL is utilized because it
can’t properly negotiate SSL.
Example:
• app/code/Chapter1/RemoteXsd
• https://lc.commerce.site/chapter11remote
Practice:
• What happens when you change the noNamespaceSchemaLocation in app/
code/Chapter1/RemoteXsd/frontend/routes.xml to be an invalid URL?
These are returned as an array, in the order specified in the original method.
Example:
// original:
namespace MyCompany\MyModule\Model;
class MyRepository
// ...
// plugin:
\MyCompany\MyModule\Model\MyRepository $subject,
$model,
$isFlatTable
) {
\MyCompany\MyModule\Model\MyRepository $subject,
callable $proceed,
$model,
$isFlatTable
) {
after also provides the parameters that were sent to the method itself,
allowing for differing alterations based on these parameters.
\MyCompany\MyModule\Model\MyRepository $subject,
$result,
$model,
$isFlatTable
) {
// do something to $result
return $result;
Sort order
Magento provides the capability to specify a sortOrder for each plugin. For
most plugins it is unnecessary or even discouraged to set the sortOrder.
However, if you, for example, need to change the input sent to another plugin
(customizing a module from the Magento Marketplace), then sortOrder would
have a valid use-case.
When there are multiple plugins for the same method in a class, here is how
it works: before plugins are executed lowest sort order, first, to highest sort
order, last. after plugins are executed high sort order, first, to lowest sort
order, last. Please read and study the example in DevDocs below and our
example for this section.
Example:
• app/code/Chapter1/Plugins
• https://lc.commerce.site/chapter12plugins/example1
Further Reading:
• Prioritizing plugins
Plugin on Plugin
Debugging
$pluginInfo = $this->pluginList->getNext($this->subjectType,'format');
if (!$pluginInfo) {
return parent::format($name);
} else {
Further Reading:
• Note: you can use the n98-magerun2.phar
config:data:di tool.
• Plugins (Interceptors)
https://lc.commerce.site/chapter12plugins/example1
(remember, this is a development machine URL: please copy the URL and paste
it into your URL bar and change the domain to whatever your development
machine runs)
https://lc.commerce.site/chapter12plugins/example2
This example proves how you can use a plugin to modify another plugin. The
best use case for this is a third party module that contains a plugin that you
need to modify. Attach your plugin to the other plugin (confusing, I know).
File: app/code/Chapter1/Plugins/Model/Plugins/Example2Plugin2.php
Practical experience:
• Locate the interceptor for MyModel class and set a breakpoint. Follow the
execution through.
Virtual types:
These are classes that are instantiated by the ObjectManager but have no
concrete class located in app/code or in generated. Confusingly, a type is
different in that the type adjusts existing classes whereas a virtualType
is "creating" a new type of class. A virtualType must extend a concrete
class type.
More concretely:
• If you wish to adjust the constructor parameters for an existing class, use the
type declaration. These changes take effect for all class instantiations for
that object in that area.
• If you wish to create an entirely new type of object that can be injected
elsewhere, use the virtualType declaration. You need to then inject or
utilize this class in XML configuration (in di.xml or as a block).
These are useful if the only necessary modifications to a class are the classes
that are injected. Additionally, virtual types do generate code (in generated).
As such, don’t use them anywhere that does not use the Object Manager (for
example, async bulk consumers).
OrderRepository">
<!-- … -->
</virtualType>
<type name="MyCompany\MyModule\DoSomething">
<arguments>
MyVirtualType</argument>
</arguments>
</type>
$repository)
/** … **./
Example:
• see app/code/Chapter1/CustomConfig.
Shared Objects:
The ObjectManager has two methods for getting an object: create and get.
create: loads a new version of the object every time. This is the method that
factories call. Note that arguments can be passed to the object’s constructor
with this method.
get: loads a "cached" version of the object (you cannot pass arguments to the
constructor because the object might have been already instantiated). The
cache is a private variable in the ObjectManager. If the object is not saved, it
is created and then saved. This is the method that is used for injecting objects
into the constructor. Arguments cannot be passed to the object’s constructor,
as Magento has no way of knowing, beforehand, whether or not the object
exists in the ObjectManager repository.
See: generated/code/Magento/Catalog/Model/ProductFactory.php
Practical Experience:
• Set a breakpoint on both the create and get methods. How is the actual
object type determined?
• \Magento\Framework\ObjectManager\Factory\Dynamic\Production
• \Magento\Framework\ObjectManager\Factory\Dynamic\Developer
Practical Experience:
• What are the differences between the production and developer factories?
How does the ObjectManager get created in the first place (hint: vendor/
magento/framework/App/ObjectManagerFactory.php)?
Proxies:
A proxy allows a class to be loaded at a later point in time. This is useful in the
case of needing to import a class with a resource-intensive function, but it is not
executed until it is needed.
return $this->_getSubject()->loginById($customerId);
if (!$this->_subject) {
? $this->_objectManager->get($this->_instanceName)
: $this->_objectManager->create($this->_instanceName);
return $this->_subject;
Additionally, proxies can be used to break a circular dependency loop. Note that
you should never directly reference the proxy class in your constructor. Rather,
use di.xml. With di.xml, you set the argument to be the \Proxy of the class
to inject. \Proxy is automatically generated and extends the base class. The
reason you should use di.xml is because PHP will throw errors if the class is
not found (hasn’t been generated yet).
Example:
// --- MyCompany/MyModule/etc/di.xml
<type name="MyCompany\MyModule\Model\ThisClass">
<arguments>
MyModule\Model\AnotherClass\Proxy</argument>
<arguments>
</type>
Further Reading:
• Proxies
Example:
• app/code/Chapter1/Proxies
• https://lc.commerce.site/chapter12proxies
Practical experience:
• What error message do you get when you remove the proxy from di.xml?
Factories:
Because all injected objects are shared, by default, there has to be a way to
create a new object. As you know, the new keyword is pretty much forbidden in
Magento development (except for throwing exceptions). If we need to create a
new Product model, importing the Product model means that any other files
that likewise import the Product model will be shared: updates made in one
class Product model will be seen in another class’ Product model—not good.
return $this->_objectManager->create($this->_instanceName,
$data);
Note that you can send an array of arguments in the $data parameter. These
arguments are sent to the constructor of the class to create:
$this->productFactory->create([
]);
Practical experience:
• How do parameters passed in through the $data parameter affect
parameters specified in di.xml?
• Let’s say you have a calculator class. You need to instantiate this calculator
class from several locations. The calculator’s constructor contains several
scalar parameters. Create a factory to make instantiation of this class easier
and typed.
If there are plugins with a higher sort order than the around plugin’s sort order,
these plugins are run before the "after" part of the around is executed. If you
are not able to follow this, please reference the additional instruction available
from SwiftOtter.com.
Practical experience:
• See example #1 from 1.2. Change the sort order of the around plugin. How
does this affect the output in https://lc.commerce.site/chapter12plugins/
example1?
Practical experience:
• Module: app/code/Chapter1/Challenge12PluginDebug
• Url: https://lc.commerce.site/chapter12pluginchallenge/
Further Reading:
• Magento application initialization and bootstrap
Further Reading:
• \Magento\Framework\Event\Manager::dispatch
• \Magento\Framework\Event\Invoker\
InvokerDefault::dispatch
• \Magento\Framework\Event\Invoker\
InvokerDefault::_callObserverMethod
• dispatch is called.
• A new event and observer are created for each event triggered.
• If you set a new value on the event’s data property, that value will
be discarded. My understanding is that the goal is to discourage
leveraging events for what plugins are intended to do.
<event name="catalog_product_save_after">
Observers\RefreshCaches"/>
</event>
Practical experience:
• Create a new observer for the catalog_controller_category_init_
after event.
Example:
• vendor/magento/module-catalog-url-rewrite-staging/etc/
adminhtml/di.xml
At this time, there are no banned observers or events for the frontend.
Practical experience:
• Ban an event and an observer on the frontend.
Options vs Arguments:
Remember this:
Most console commands accept options. A few offer arguments. In the example
listed below, the argument is an array (separated by spaces) of languages:
Example:
• app/code/Chapter1/CLI
• bin/magento customer:create
Run:
Practical Experience:
• Create a new CLI command to execute:
• Call $this->state->setAreaCode().
Important Notes
• Calling getAreaCode() when no area code is set throws an
exception.
• Example: app/code/Chapter1/CLI
• bin/magento trigger:event
Example:
• app/code/Chapter1/CLI
• bin/magento trigger:event:area
Practical experience:
• Determine what features do change when emulating an environment.
Further Reading:
• Emulating Areas in Magento 2
Warning:
I am barely able to scratch the surface of uiComponents. It
is imperative that you study this section, work through the
practical examples, and understand the sample code before
attempting the test.
Note:
Note: with uiComponents, everything is a component. That
means every uiComponent extends uiElement (vendor/
magento/module-ui/view/base/web/js/lib/core/
element/element.js). Every uiComponent is a child of
another uiComponent. The custom uiComponents you create
are children of uiComponents. This is fundamental knowledge.
Workflow:
Beyond the standard requirements to serve an HTML page, these files are
required for a UiComponent:
• (nothing else)
As you can see, there are very few files required. While some of the
uiComponent XML structure makes little sense, drawing up even basic
uiComponents is quite fast.
Beyond the basics, here are some helpful ancillary files that you will
probably create:
• Actions column which will provide the actions drop down menu column.
Extend \Magento\Ui\Component\Listing\Columns\Column.
Note that you also must tell the DataProvider collection factory that there are
more collections available. This usually happens in di.xml, like:
<type name="Magento\Framework\View\Element\UiComponent\
DataProvider\CollectionFactory">
<arguments>
<item name="custom_product_grid_data_source"
xsi:type="string">CustomProductGridCollection</item>
</argument>
</arguments>
</type>
<referenceContainer name="content">
<uiComponent name="custom_product_form"/>
</referenceContainer>
Often, the host layout XML files are quite empty, as they just contain the
uiComponent instruction.
Execution:
• \Magento\Ui\TemplateEngine\Xhtml\Result::__toString() is
called with the express purpose of rendering the uiComponent.
Data loading:
Once the Javascript files on the page have loaded, the uiComponent triggers a
request to: vendor/magento/module-ui/Controller/Adminhtml/Index/
Render.php. This controller is specified with the listing/dataSource/
dataProvider/data/config/update_url path (note that the latter items are
in argument and item nodes).
So, how does the render class know where the data is? Easy: the request
must specify a namespace parameter. The namespace is really the name of
the uiComponent. Add .xml to it and search through your codebase for a
file with that name (ex. custom_product_grid.xml), and you will find the
uiComponent.
Forms data:
The default data wrapper name is general. Inside the general array, you will
find the data for your form.
In the example, our form has multiple tabs. Please note that the form will not
save as this is simply a demonstration of uiComponent forms. Pay special
attention to how the data is mapped.
To better understand how this works, run these commands in your browser’s
developer tools:
require('uiRegistry').get('index = name');
links:
value: "custom_product_form.custom_product_form_data
source:data.general.name"
require('uiRegistry').get('custom_product_form.custom_product_
form_data_source').data
It is important to note that you can insert regular layout instructions into your
uiComponent. For example, in the Customer Form uiComponent (vendor/
magento/module-customer/view/base/ui_component/customer_form.
xml), you will find the following code. Notice that the htmlContent is wrapping
the regular layout XML:
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/
ui_configuration.xsd">
<!-- … -->
<htmlContent name="customer_edit_tab_view_content">
<block class="Magento\Customer\Block\Adminhtml\Edit\
Tab\View" name="customer_edit_tab_view"
template="Magento_Customer::tab/view.phtml">
<arguments>
</argument>
translate="true">Customer View</argument>
</arguments>
<block class="Magento\Customer\Block\Adminhtml\Edit\
Tab\View\PersonalInfo" name="personal_info"
template="Magento_Customer::tab/view/personal_info.
phtml"/>
</block>
</htmlContent>
<!-- … -->
</form>
Grid examples:
• vendor/magento/module-customer/view/adminhtml/ui_component/
customer_listing.xml
• vendor/magento/module-catalog/view/adminhtml/ui_component/
product_listing.xml
Form examples:
• vendor/magento/module-customer/view/base/ui_component/
customer_form.xml
• vendor/magento/module-catalog/view/adminhtml/ui_component/
product_form.xml
Note:
Note that an example and more details of this are available in
the SwiftOtter learning kit.
Practical experience:
• Set a breakpoint in __construct in vendor/magento/framework/View/
Element/UiComponent/DataProvider/DataProvider.php (or your
custom DataProvider) and observe how the dataProvider node in the
uiComponent configuration matches the parameters. Doesn’t this open
further configuration possibilities?
• Should you run into issues with changing a uiComponent, feel free to
modify the DataProvider.
Further Reading:
• https://www.mage2.tv/ has a large amount of detailed
instruction on uiComponents.
require('uiRegistry').get('custom_product_grid.custom_product_grid')
You can access the base component by first calling the name of the
uiComponent (custom_product_grid) and then the name of the columns
specified (custom_product_grid), separated by a ..
require('uiRegistry').get('custom_product_grid.custom_product_grid_data_source')
You can also get an item with the index command, instead of using the full
component name:
require('uiRegistry').get('index = name');
Further Reading:
• uiRegistry
By default, data providers are responsible for storing and managing the data
saving process. Confusingly, a data storage component loads the data from
the server.
Forms:
Provider: vendor/magento/module-ui/view/base/web/js/form/
provider.js. The data is specified in the initialization JSON. It is then
accessible in the data source component.
Grids:
Provider: vendor/magento/module-ui/view/base/web/js/grid/
provider.js
The data storage component is responsible for loading data. Note that the
initialization JSON does have data specified, but that data is quickly discarded.
Practical experience:
• Set a breakpoint in vendor/magento/module-ui/view/base/web/js/
grid/data-storage.js in Admin > Catalog > Products (custom) to see
how the data is loaded in.
Further Reading:
• vendor/magento/module-ui
Practical Experience:
• Set a breakpoint in \Magento\Ui\Component\AbstractComponent::__
construct. Understand how the values that come into the $data argument
originate in the XML uiComponent definition.
uiClass: vendor/magento/module-ui/view/base/web/js/lib/core/
class.js
\/
uiElement: vendor/magento/module-ui/view/base/web/js/lib/core/
element/element.js
\/
uiComponent: vendor/magento/module-ui/view/base/web/js/lib/
core/collection.js
uiClass:
This is the basic building block for any uiComponent or related element. This
provides the basic functions necessary to propagate and extend.
uiElement:
This represents a basic element—one that does not need the ability to
manage children.
Example:
• vendor/magento/module-ui/view/base/web/js/grid/resize.js
uiComponent:
Most uiComponents extend this class. This has the capability of storing and
rendering multiple children (similar to blocks in Magento layout).
Example:
• vendor/magento/module-ui/view/base/web/js/grid/toolbar.js
Further Reading:
• Layout File Types
• vendor/magento/magento-theme
Custom handles are usually easy to add (an exception being a category page).
Our controller (app/code/Chapter2/Layout/Controller/Index/Index
.
php)
contains an example of how to do this.
• Are you sure that you have the correct layout XML file? It is easy to confuse
the router id and the router front name.
• Set a breakpoint on your block or view model’s class line. This will tell you
when the autoloader includes your file, and you can continue until you locate
where Magento skips over your block.
Example:
• https://lc.commerce.site/index.php/chapter22layout/ (handles)
• app/code/Chapter2/Layout
• https://lc.commerce.site/index.php/chapter22layout/index/index/xml/1
(displaying XML layout)
Further Reading:
• Adding Custom Layout Handles in Magento 2
<!-- … -->
</container>
• htmlId
• htmlClass
Further Reading:
• Container Structure
A side effect of this is that once build() has been called, any additional layout
handles added will have no effect on the Layout XML that is loaded. As such, it
is imperative to add layout handles as early as possible.
Example:
• https://lc.commerce.site/index.php/chapter22layout/index/addblock
• app/code/Chapter2/Layout/Controller/Index/AddBlock.php
Block caching
By default, blocks are not cached (unless a block is included in a parent block’s
cache). To cache a block, you must specify the cache_lifetime key in the
block’s _data variable and have the block_html cache enabled. Because
Magento 2 has integrated full page caching so well, I believe that block caching
is less relevant than it used to be.
Note that you can disable the entire page’s caching by specifying
cacheable="false" in a block, like:
block.phtml" cacheable="false">
<arguments>
MyModule\ViewModel\CustomerInfo</argument>
</arguments>
</block>
There are few use cases where this makes sense. Unfortunately, there are
modules in circulation out there that misuse this directive and end up excluding
half or more of a website from being full-page cached.
You can use it to prevent customer data from being cached. However, in these
cases, it is usually better to use AJAX requests (and the API) to load the
respective data. You can also utilize Magento’s ESI implementation called Private
Content. You can also search the Magento codebase to find the cases where
this is used.
Further Reading:
• Page Caching
Example:
• \Magento\Email\Model\Template\Filter::emulateAreaCallback
Practical experience:
• Step through \Magento\Framework\View\Element\AbstractBlock::_loadCache
Further Reading:
• Private Content
Fallback debugging
The core class that renders templates is: \Magento\Framework\View\
Element\Template. The getTemplateFile() method is responsible for
locating the specific file to be used in this situation.
Further Reading:
• Layout Instructions
app/design/frontend/MyCompany/MyTheme/Module_Name/templates/path/
to/template.phtml
Example:
• https://lc.commerce.site/index.php/chapter23blocks
• app/code/Chapter2/Blocks
Practical experience:
• Set a breakpoint in \Chapter2\Blocks\Block\ExampleBlock. Step
through line-by-line to understand how the template resolver works.
• Create a module to change the template for the Luma theme’s menu.
Email templates
Email template fallbacks are supported in the same way that regular block
templates are supported. Instead of email templates being located in the view/
[area]/templates/ directory, they are found in view/
[area]/
email.
Further Reading:
• Customize Email Templates
Translations
The above command will generate a translation file for your module that you
can then send to someone to translate.
Further Reading:
• Translations Overview
Keep in mind that a virtualType would be a perfect fit here: define the virtual
type and set the cache_lifetime. Then, utilize that virtual type in layout XML.
Note that you cannot implement plugins on a virtual type, but instances, where
this is necessary, are very rare.
Example:
• https://lc.commerce.site/index.php/chapter23blocks
• app/code/Chapter2/Blocks
• Ensure full_page cache is off, but block_html cache is on. Then, refresh
the page several times and note how the upper time does not change, but
the lower time does.
$transport = $this->transportBuilder->setTemplateIdentifier($templateId)
->setFrom($from)
->getTransport();
$transport->sendMessage();
If you want to intercept the variables before they are applied to the
email message, the easiest place is likely: \Magento\Email\Model\
AbstractTemplate::getProcessedTemplate.
See \Magento\Newsletter\Model\TemplateTest::testGetProcessedTemplateArea
for an example of how this works.
{{view url="Magento_Theme::favicon.ico"}}
Further Reading:
• \Magento\Customer\Model\
EmailNotification::passwordReset
• \Magento\Email\Model\Template\
Filter::viewDirective
• \Magento\Framework\View\Asset\
Repository::getUrlWithParams
Practical experience:
• Hopefully, you easily know how to find this by now: if you don’t, here
is the answer. Set a breakpoint on the class Template line in \
Magento\Framework\View\Element\Template and reload a Magento
frontend page.
• Look back through the call stack to determine where the block is
instantiated. Note that everything down to the ObjectManager is standard
Magento instantiation. The next class or two would be considered
the answer.
Answer:
\Magento\Framework\View\Element\BlockFactory::createBlock
\Magento\Framework\View\Layout\Generator\Block::getBlockInstance
\Magento\Framework\View\Layout\Generator\Block::createBlock
• vendor/magento/theme-frontend-blank/Chapter2_Blocks/
templates
• app/code/Chapter2/Blocks/view/frontend/templates
• app/code/Chapter2/Blocks/view/base/templates
Source: \Magento\Framework\View\Design\FileResolution\Fallback\
Resolver\Simple::resolveFile
Inline
This happens with the now-famous __() method. This method is available
anywhere (from vendor/magento/framework/Phrase/__.php).
By default, there are three translation renders. Each one receives the output
from the previous renderer:
• \Magento\Framework\Phrase\Renderer\Translate: this
class does what we would expect from a translation renderer. It
translates the input text. Ultimately, the translation is loaded from
\Magento\ Framework\ Translate.
Example:
• app/code/Chapter2/Blocks/view/frontend/templates/translation.phtml
CSV file
These are the heart of developer-supplied translations—CSV files that are easily
transferred from translator to developer allow for quick translation updates.
Practical experience:
• Create a new translation file in your module. Using en_US.csv as the default
works fine.
• Create a template that utilizes the __() translation method and set a
breakpoint there to understand how translation works.
• Set breakpoints in app/code/Chapter2/Blocks/view/frontend/
templates/translation.phtml to understand how this logic works.
Example website:
• https://lc.commerce.site/index.php/chapter3database
Models
Models represent a row of data from the database. Each row, whether it is
loaded through a collection or a resource model, is represented by the model.
Few, if any, true Magento data models will utilize the constructor for
dependency injection. Best practice is to have data models as a storage
container for storing data so that it is easily utilized.
Note:
When creating an API that returns a model, you do not have
to return “the” data model that was loaded from the database.
Feel free to create a new model and inject “the” data model
that was loaded. The new model acts as a proxy and modifies
output values. Another technique is to utilize two interfaces:
one for the internal application and one for the external
application. We will discuss this more later.
Example:
• app/code/Chapter3/Database/Model/Discount.php
Resource models
This is where stuff gets done. The resource model is what communicates with
the database. This is where you should put most custom queries. The resource
model is hooked up to the model, collection, and repository.
Example:
• app/code/Chapter3/Database/Model/ResourceModel/Discount.php
Collections
A collection loads multiple entities from the database. Their use is greatly
diminished with the advent of repositories (although both share very similar
Collections extend:
\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
Example:
• app/code/Chapter3/Database/Model/ResourceModel/Discount/
Collection.php
Repositories
featured, you do not have to write a repository that uses all these features. The
repository we are creating for our discount model is based on the \Magento\
Cms\Model\BlockRepository.
Example:
• app/code/Chapter3/Database/Model/DiscountRepository.php
• Inject:
• \Magento\Framework\Api\SearchCriteria\
CollectionProcessorInterface to convert a \Magento\
Framework\Api\SearchCriteriaInterface into filters that a
collection understands.
• Create etc/webapi.xml
SearchCriteria
This is an ingenuitive and new way to locate records. You attach a long url
parameter to a request (that accepts SearchCriteriaInterface, such as a
getList method).
In our sample application, note that to get all items, simply pass a null value
for the criteria parameter.
'searchCriteria[filter_groups][0][filters][0][condition_type]=' + conditionType;
return fetch(url, {
credentials: 'include',
headers: {
'Content-Type': 'application/json'
Example:
• app/code/Chapter3/Database/view/frontend/web/js/api.js
Practical experience:
• Create an API request (curl, fetch, or PHP) to request /rest/V1/products
(in the admin panel, so keep sessions in mind). Ensure you are familiar with
creating filter groups and filters.
Further Reading:
• Search using REST endpoints
• APIs do not allow you to set cookies. If this is required (like logging a user in),
you must use a controller, which can be accompanied by a JSON response.
• <resource ref="Chapter3_Database::discounts"/>:
authenticated with the admin ACL.
Examples:
• app/code/Chapter3/Database/etc/webapi.xml
• app/code/Chapter3/Database/etc/acl.xml
Extension attributes
<extension_attributes for="Magento\Directory\Api\Data\
CountryInformationInterface">
Database\Api\Data\CurrencyInformationInterface" />
</extension_attributes>
Further Reading:
• Configure services as web API
For the above extension attribute declaration, let’s make some observations:
• The generated files are in the same namespace as the for class or interface.
• The generated files replace Interface (if it exists) and add Extension,
ExtensionInterface and ExtensionInterfaceFactory to the class or
interface name.
If you are adding a new extension attribute to a class whose extension files
have already been generated, you will need to delete the generated files for
that class.
Our example will inject a dummy class into a list of directory countries.
Practical experience:
• Set a breakpoint in \Chapter3\Database\Plugin\
SetCurrencyInformation::afterGetCountriesInfo.
Further Reading:
• An Introduction to Extension Attributes
// vendor/magento/module-sales/Model/Order.php
/**
* @return \Magento\Sales\Api\Data\OrderExtensionInterface|null
*/
return $this->_getExtensionAttributes();
Practical experience:
• Set a breakpoint in \Magento\Framework\Api\Code\Generator\
ExtensionAttributesGenerator::_generateCode.
Further Reading:
• vendor/magento/framework/Api/
ExtensionAttributesFactory.php
Note:
That this is a Commerce-only feature until Magento 2.3, when
it is available for Open Source, too.
This is a tremendous feature and our example should make the learning curve of
this much easier.
• A publisher, to format the data such that it can be stored and transported to
the queueing system.
• A consumer, to load the data and perform the necessary actions. This
operation is manually started (think supervisor) to receive the messages
coming from RabbitMQ.
• Scheduler (\Queueing\BulkSave\Operations\
BulkDiscountScheduler::execute). This publishes the operation data to
the message queue.
As you review the above code examples, you will see that a large portion of the
code is gracefully handling error cases.
Example:
• See \Queueing\BulkSave\Controller\Index\Index (this is in a
different namespace as the message queue system and does not use the
Magento Object Manager, thus no virtual types, and cannot have numbers in
the class path).
• Note that you can utilize MySQL, but I wrote this in RabbitMQ so you get
the full picture.
'amqp' =>
array (
),
Further Reading:
• Bulk Operations
remote system could be time intensive and make an API request come to a
grinding halt.
Further Reading:
• How to Use Data-related Classes, Repositories and Data API
in Magento 2
https://lc.commerce.site/rest/V1/testOverride/joseph/
to
https://lc.commerce.site/rest/V1/testOverride/joseph/maxwell/
Practical experience:
• In your browser, visit: https://lc.commerce.site/rest/V1/testOverride/
Joseph%20Maxwell
You can also override the existing API definition by simply specifying the same
URL and method but with a different interface. You must also ensure that your
module’s sequence is after the module you are overriding.
Practical experience:
fetch('https://lc.commerce.site/rest/V1/postOverride', {
method: 'post',
headers: {
"Content-Type": "application/json"
},
log(response));
fetch('https://lc.commerce.site/rest/V1/postOverride', {
method: 'post',
headers: {
"Content-Type": "application/json"
},
'Maxwell'}),
log(response));
Example:
<type name="Magento\Framework\Authorization">
Plugin\GuestAuthorization" />
</type>
PHP: \Magento\Webapi\Model\Plugin\GuestAuthorization::aroundIsAllowed
One of the fundamental changes that arise as a result is how Magento uses
row_id as an entity table’s primary key (instead of entity_id).
catalog_product_entity table:
catalog_product_entity_varchar table:
staging_update table:
• All attribute value tables reference the entity table’s row_id and not the
entity_id.
Practical experience:
Locate the table that is keyed to the row_id for the products
(hint: sequence_…).
Further Reading:
• vendor/magento/module-catalog-staging
Entity Manager
Information:
Study vendor/magento/module-staging/Model/
VersionInfoProvider.php.
If you wish to learn more about the entity manager, see the appendix at the end
of the book.
This is the deepest dive that I have taken into this mechanism in Magento.
However, I wasn’t seeing any code related to this in Magento. Doing some
research, I found that Magento released CMS version control in Magento 2.0 but
then removed in Magento 2.0.1 (vendor/magento/module-versions-cms/
Setup/UpgradeSchema.php).
Important points:
• The created_in and updated_in columns reference the id column in
staging_update. All are Unix timestamps.
• A transaction is created.
• The version manager has the new scheduled update version set.
• See: \Magento\CmsStaging\Model\PageStaging::schedule
Practical experience:
• Set a breakpoint in \Magento\Staging\Model\Entity\Update\
Save::execute.
To load a product from the database, the Magento Open Source query would
look something like:
As such, you need to load a specific row for an entity_id. Here is an example
SQL query:
'1549763040')
• Then, this class will load metadata from the metadata pool for this table.
• \Magento\Staging\Model\ResourceModel\Update::getMaxIdByTime is
called. This method converts the requested timestamp into a \DateTime
object. (SELECT * FROM staging_update WHERE start_time <=
[TIMESTAMP] ORDER BY `id` DESC LIMIT 1). The current version or
the next most recent (but not in the future) will be selected.
Note:
The flag table stores information that isn’t configuration or
cacheable but needs to be updated or used on a regular basis.
Practical experience:
• Schedule a product modification.
• Preview it.
• Follow the path through the stack trace to find where the specific version is
set. Observe how this is set and where the versions are pulled from.
Further Reading:
• vendor/magento/module-staging/Model/Select/
FromRenderer.php
• vendor/magento/module-staging/Model/Update/
VersionHistory.php
$versionManager->setCurrentVersionId($versionId);
$this->productRepository->getById($productId);
If you wish to disable the additions for staging, and you have access to the
Select object, add a part to the Select:
$select->setPart(‘disable_staging_preview', true);
Practical example:
• Log into your Magento admin and create some staging versions for product
ID #1 (24-MB01)
`product`.`updated_in`
Practical example:
• Log into your Magento admin and create some staging versions for product
ID #1429 (WS03-M-Red)
Further Reading:
• vendor/magento/module-catalog-staging/Model/
Product/Locator/StagingLocator.php
• Finds all rows that have the created_in between the last-enabled version
and the current version (timestamp).
The performance implications are that each item is being saved. If there are
many items affected by the scheduled update, this could have a major impact
on the website, and possibly make the frontend unresponsive, unless varnish
has been properly configured.
Practical investigation:
• vendor/magento/module-cms-staging/etc/di.xml
Original (vendor/magento/module-cms/etc/di.xml):
<!-- … -->
<type name="Magento\Framework\EntityManager\MetadataPool">
<arguments>
</item>
<!-- … -->
</argument>
</arguments>
</type>
<!-- … -->
Modified (vendor/magento/module-cms-staging/etc/di.xml):
<type name="Magento\Framework\EntityManager\MetadataPool">
<arguments>
</item>
<!-- … -->
</argument>
</arguments>
</type>
Original (app/etc/di.xml):
<type name="Magento\Framework\EntityManager\OperationPool">
<arguments>
Magento\Framework\EntityManager\Operation\
CheckIfExists</item>
Framework\EntityManager\Operation\Read</item>
Framework\EntityManager\Operation\Create</item>
Framework\EntityManager\Operation\Update</item>
Framework\EntityManager\Operation\Delete</item>
</item>
</argument>
</arguments>
</type>
Modified:
<type name="Magento\Framework\EntityManager\OperationPool">
<arguments>
<item name="Magento\Cms\Api\Data\PageInterface"
xsi:type="array">
Staging\Model\Operation\Create</item>
Staging\Model\Operation\Update</item>
Staging\Model\Operation\Delete</item>
</item>
</argument>
</arguments>
</type>
Setup scripts are quite similar but categorized for specific purposes (data,
schema, etc.).
Practical examples:
• Run bin/magento module:enable Chapter3_SetupScripts
• app/code/Chapter3/SetupScripts
Install scripts run when the module is not found in setup_module table, and
upgrade scripts run when the module is out of date.
SET SQL_MODE=''"
As you can see, calling startSetup disables foreign key checks and allows 0
not to trigger, replacing itself with the next auto increment ID.
Further Reading:
• Setup scripts in Magento 2
Recurring scripts
Recurring scripts run anytime setup:upgrade is run. These scripts run after the
schema or data scripts are executed.
Practical examples:
• \Chapter3\SetupScripts\Setup\Recurring
• \Chapter3\SetupScripts\Setup\RecurringData
Uninstall scripts
There is one uninstall script (combined for data and schema). The only
example in the Magento code is: \Temando\Shipping\Setup\Uninstall.
If you wish to include an Uninstall script, place it in your module’s
Setup/ php file.
Uninstall.
• Schema recurring
• Data recurring
Practical experience:
• Create a breakpoint in:
\Magento\Setup\Model\Installer::handleDBSchemaData
Further Reading:
• Setup scripts
Practical experience:
• Set a breakpoint in:
\Chapter3\SetupScripts\Setup\Recurring::install
Practical example:
• \Chapter3\Staging\ViewModel\JoinProducts::getOrderItems
Additionally, the user interface will also slow down as KnockoutJS is not
optimized for handling large amounts of data.
Further Reading:
• \Magento\Catalog\Ui\DataProvider\Product\Form\
Modifier\AttributeSet
This can also cause problems with indexing in the flat entity tables. Flat entity
tables are not per attribute set and thus are an aggregation of all attributes.
You could run into MySQL maximum column limits. As of MySQL 5.6.9, the
maximum number of columns is 1017. It should be quite simple to pare back the
number of attributes that are marked as “Used in Product Listing.”
Finally, so many attributes could bring the admin product edit page to an
unusable crawl. Again, KnockoutJS is not designed for high-performance.
Further Reading:
• Error Code 1117 Too many columns; MySQL column-limit
on table
Note:
Note: \Magento\Catalog\Api\Data\
ProductAttributeInterface has many default product
attribute codes already defined in an @api. Anytime you can
use methods or constants specified in the API, your code has
less chances of breakage.
• If you specify a group key in the attribute settings, Magento will either create
a group or add this attribute to an existing group. Remember, an attribute set
has attribute groups which have attributes. Additionally, if the group key is
specified, the attribute will be added into all attribute sets. Do not specify the
group if you want to control which attribute sets the attribute is added to.
• Reference: \Magento\Eav\Setup\EavSetup::addAttribute
Customer attributes require adding the attribute to forms, after the attribute is
created. See the practical example for how to do this.
Practical example:
• \Chapter4\EAV\Setup\InstallData::install Feel free to execute this
as a PHP script in PHPStorm. Set a breakpoint on the addAttribute line.
• \Chapter4\EAV\Setup\UpgradeData::updateProductAttribute
• \Chapter4\EAV\Setup\UpgradeData::createCustomerAttribute
Further Reading:
• How to Add a New Product Attribute
Then, for each entity type, there are several tables that store values (using
catalog_product as the example):
Note that the entity-level attribute properties are discussed above in 4.1.
By default:
• datetime
• decimal
• int
• text
• varchar
To create a new backend type, create an attribute with the custom backend
type specified. You will need to either specify the value for the attribute’s
backend_table or you need to create a new table for that entity with
the type. For example, if you wanted to create a new JSON backend type
for the catalog_
product entity type, you would create a new table:
product_entity_json to host the data.
catalog_
You must manually create the column for the static attribute.
Attribute options are loaded via the attribute’s source class. The source
class must extend \Magento\Eav\Model\Entity\Attribute\Source\
AbstractSource. Note that if you have a custom source class and you are
utilizing flat tables and your attribute is not added to the flat tables, you need to
specify some additional information. Look at the getFlatColumns() method
in the aforementioned abstract class.
If you wish to initially populate options for an attribute (and let the admin
control the values henceforth), make sure to set the input (create attribute)
or frontend_input (update attribute) to be multiselect or select. Then,
when no custom source_model is specified \Magento\Eav\Model\Entity\
Attribute\Source\Table is automatically assumed.
You can call addAttributeOption on the EavSetup class for each option you
want to add.
Further Reading:
• \Magento\Customer\Setup\
UpgradeData::upgradeVersionTwoZeroTwo
If you need to create shared options between entities, you must create a
custom source_model for your attribute. This will load rows from the pertinent
table and make those available in the getAllOptions method.
Why would you, as a Magento developer, add items to this table? You would do
so if you need to give the store’s administrator an easy place to manage these
values or to allow easy customization per store. However, it is very inflexible
for future updates to this information. As such, we do not use the eav_
attribute_option tables.
To make a customer or customer address attribute save, you must add the
attribute to the correct form. Also, you must specify the group = General for
non-catalog entities.
To make the inputs appear on the frontend, you must update the customer
form or customer address form templates to add the appropriate inputs.
the SalesSetup class adds a column to the sales_order table and the
sales_order_grid table (actually, for any table referenced in that class’
flatEntityTables property).
$_
Practical examples:
• \Chapter4\EAV\Setup\UpgradeData::createCustomerAttribute
• \Chapter4\EAV\Setup\UpgradeData::createCustomerAddressAttribute
• \Chapter4\EAV\Setup\UpgradeData::createOrderAttribute
Further Reading:
\Magento\Eav\Model\ResourceModel\Attribute
::_afterSave is where the attributes are added to the forms.
In my tests, saving an attribute via the AttributeRepository did
not add the attribute to the forms. Rather, I had to call save
directly on the attribute.
This switch also tells the Customer Attribute management module whether or
not the attribute can be deleted (see \Magento\CustomerCustomAttributes\
Block\Adminhtml\Customer\Formtype\Edit::_construct).
My Account
Checkout
Customer addresses are not directly visible in the checkout. You must configure
the frontend uiComponent.
Further Reading:
• Add a new field in address form
Admin pages
As discussed above, you must ensure that the attribute is present in the
customer_eav_attributes table (and, of course, clear the cache).
Further Reading:
• \Magento\Authorization\Model\Acl\Loader\
Rule::populateAcl
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<acl>
<resources>
<resource id="Chapter5_ACL::all_actions"
</resource>
</resources>
</acl>
</config>
Note that the admin ACL entries are always found in the
Backend::admin resource (otherwise, you will not be able to
Magento_
administer these entries). Below this node, you can create as complex of a
structure as necessary.
WebAPI ACL
The API is tightly integrated with the ACL. Every API route declaration can be
associated with a ACL resource (notice how the ref matches):
<resources>
<resource ref="Chapter5_ACL::all_actions"/>
</resources>
</route>
Note that the entry point for the WebAPI ACL is: \Magento\Framework\
Webapi\Authorization::isAllowed. This allows a separation between the
Magento Admin ACL and the API ACL. The API authorization mechanism also
allows for anonymous and self (customer session). It does not work for the
admin session as the API always initializes the frontend session.
\Magento\Authorization\Model\Acl\AclRetriever::_canRoleBeCreatedForUserType
Further Reading:
• Authentication
Practical experience:
• Enable Chapter5_ACL.
• \Magento\Webapi\Model\WebapiRoleLocator::getAclRoleId
• Then run the file (PHPStorm’s run PHP scripts works well), or:
• php -f app/code/Chapter5/api-test.php
• You will need to change the username and password to ones that work in
your test environment.
Further Reading:
• Relation between acl.xml and webapi.xml
Row-based ACL is authorizing access by row. In other words, the concept is that
specific rows in the database are authorized or denied. For example, this would
be specific products are allowed and others are denied.
Magento provides a glimpse into how this works in the AdminGws module.
Instead of using the isAllowed method, they interact with each touchpoint
throughout the Magento admin (and there are a lot). This allows more fine-
grained control to prevent users from even seeing objects that they are not
allowed to see.
Further Reading:
Study the approach that Magento takes in the Magento\
AdminGws module.
Finally, Magento allows Oauth 1.0 (important that this is NOT Oauth 2.0).
This allows a user to configure access.
Further Reading:
• Authentication
• aroundDispatch checks:
• If the user is logged in, the ACL permissions are refreshed for the session.
• \Magento\User\Model\User::login(), then
\ User\Model\User::authenticate() is called.
Magento\
As you can see, there are many access points to customizing the admin
login process.
Practical experience:
• Set a breakpoint in:
\Magento\Backend\App\Action\Plugin\Authentication::aroundDispatch
Other examples:
• Google Backend Login
• MSP TwoFactorAuth
• Call \Magento\Backend\Model\Auth\StorageInterface::setUser($user) to
assign the user with the current admin session.
• Call \Magento\Backend\Model\Auth\StorageInterface::processLogin() to
setup the session and refresh the ACL.
Further Reading:
• MSP TwoFactorAuth
• Specifically: ControllerActionPredispatch.php
If you look at this class, you will see it is blank. This class extends \Magento\
Backend\App\AbstractAction.
• dispatch(): ensures that the user is logged in and that they have access
to the const ADMIN_RESOURCE method. Note that descendant controller
actions can override this const instead of specifying the _isAllowed
method. Attaching a plugin to this method ensures that your functionality
will be executed on every admin page.
Further Reading:
• \Magento\Backend\App\AbstractAction
• If you want to customize the data that is going to the column, you can
specify a class attribute that extends the \Magento\Ui\Component\
Listing\Columns\Column column.
• You can also set a settings node, or use <argument name="data" .../>
for a custom settings tree.
• app/code/Chapter5/BackendCustomization/view/adminhtml/ui_
component/custom_customer_grid.xml
Practical example:
• In app/code/Chapter5/BackendCustomization
Further Reading:
• How inline edit works in admin ui-components grid
Magento2?
• If you want to change the HTML template, create a new HTML template and
reference that in the template property.
<listing>
<settings>
<buttons>
<button name="add">
<url path="*/*/edit"/>
<class>primary</class>
</button>
</buttons>
</settings>
</listing>
Practical example:
• app/code/Chapter5/BackendCustomization/view/adminhtml/ui_
component/custom_customer_form.xml
Further Reading:
• Declare a custom UI Component
Note that, by default, Magento puts fields in different fieldsets into an array
with that fieldset’s name. For example, general is the default fieldset
name. All fields inside general will be POSTed in the $_POST['general']
array, like $_POST['general']['my_field_name']. If you create another
fieldset named customer, all fields in that fieldset would be accessed in
POST['customer']['customer_name']. The use of $_POST in the
$_
example is for the sake of simplicity.
If you wish to keep all POST data in the same array, specify
settings/dataScope, like:
fieldset/
<fieldset name="customer">
<settings>
<dataScope>data.general</dataScope>
</settings>
</fieldset>
Further Reading:
• How to add Tab in Form Ui Component
• Fieldset Component
Add a htmlContent element. This element will somewhat switch the "mode"
from uiComponents to layout XML.
Further Reading:
• vendor/magento/module-rma/view/base/ui_
component/customer_form.xml
Further Reading:
• vendor/magento/module-ui/view/base/web/js/
grid/editing/record.js
<bookmark name="bookmarks"/>
You can also specify the namespace in which to store the bookmark data:
<bookmark name="bookmarks">
<settings>
<storageConfig>
<namespace>my_namespace</namespace>
</storageConfig>
</settings>
</bookmark>
Further Reading:
• How to use Magento 2 UI Components
This happens in the DataSource. See the practical example below for a sample
on how to complete this.
Practical example:
• app/code/Chapter2/UiDemonstration/Plugin/AddProductDetailsToDataProvider.php
• Configurable
• Group
• Bundle
• Downloadable
You can create a configurable product that has custom options. Note that you
are unable to specify a percent price type for a custom option.
However, a grouped product only allows simple and virtual product types as
participants in its selection choice (see vendor/magento/module-bundle/
etc/product_types.xml).
Practical experience:
• Create a configurable product and assign some custom options to it. How
does it appear on the frontend?
• While we assume we are experts in the Magento admin panel, are we? Ensure
you have tested combinations and somewhat memorized the admin panel.
2. If the product will be saleable, add the product type to your module’s
etc/sales.xml in the order/available_product_type node (see
vendor
/magento/module-bundle/etc/sales.xml).
3. For any attributes that you wish to have applied to this new product type,
you may need to update the catalog_eav_attribute table. By default,
apply_to is blank and a blank value means that this attribute applies to all
product types. However, if apply_to is not blank, you need to append the
new product type to this list.
6. Add your product type to the admin and frontend renderers (see the view/
[area]/layout directory in vendor/magento/module-bundle).
sortOrder="50">
<indexerModel instance="Magento\Bundle\Model\ResourceModel\
Indexer\Price" />
<stockIndexerModel instance="Magento\Bundle\Model\
ResourceModel\Indexer\Stock" />
<allowedSelectionTypes>
</allowedSelectionTypes>
<customAttributes>
</customAttributes>
</type>
Important notes:
• Note the separate price and indexer models. Price calculates the product
price on the product page and thereafter. The indexer calculates the price
before the product page (search and category).
Further study:
• vendor/magento/module-bundle/
• vendor/magento/module-grouped/
Magento product types solve for the majority of use cases but not everything.
We solved for this on SwiftOtter.com by creating a new product type: Test. This
allows us to build custom functionality surrounding the handling and layout of
this product type.
Having a large number of product relationships will slow down both editing the
product as well as the product view page (possibly the shopping cart page, too).
Related products are an add-on product that complements the product being
viewed. These products are easily added to the cart with a checkbox.
Further Reading:
• Related Products
• Up-sells
Related products:
The most performant way to get the list of related products is to go to the
Link resource model: Magento\Catalog\Model\ResourceModel\Product\
Link::getChildrenIds($parentId, $typeId). You can also load specific
relation types through the Magento\Catalog\Model\Product class (for
example, getUpSellProductCollection()).
Custom options:
Configurable parameters:
The \Magento\ConfigurableProduct\Model\Product\Type\Configurable
class contains easy ways to obtain information about the attributes and
product that are associated with a configurable product. Note that this class
can be accessed from a product’s $product->getTypeInstance() method.
However, blindly assuming that you are working with a Configurable class may
yield some PHP fatal errors, so ensure you do proper type checking.
• Special price: a price reduction that is in effect for a specified period of time.
• Base price (accepts qty parameter): minimum of the tiered price and
special price.
Practical example:
• Set a breakpoint on line ~33 (renderAmount), and step through the chain for
each product type.
• vendor/magento/module-configurable-product/view/base/
templates/product/price/final_price.phtml
• \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price
• \Magento\Bundle\Model\Product\Price
• \Magento\Downloadable\Model\Product\Price
• \Magento\GiftCard\Model\Catalog\Product\Price\Giftcard
• \Magento\GroupedProduct\Model\Product\Type\Grouped\Price
Configurable product
Calling getPrice ("Price" above) checks to see if a child product has been
set. If it has, the price is calculated based on the child product. Otherwise, 0
is returned.
Bundle product
Overrides the Final Price calculation to add the total bundle items price to the
base price. Remember that bundle products still have a base price.
Downloadable product
Giftcard
Overrides Price and Final Price to specify the custom option value for the
amount that was specified.
Grouped
Overrides the Final Price calculation to add the totals for the associated
(child) products found in the associated_product_[child product id]
custom option.
Special price
Special pricing has two extra fields: start and stop dates for the new price. You
might notice that these fields are missing in Commerce. The reason is that
Commerce uses the staging start and stop dates. As such, the special_from_
date and the special_to_date fields are loaded the same as
normal attributes.
Further Reading:
• Magento\CatalogStaging\Observer\
UpdateProductDateAttributes::execute
Price rendering
$this->getLayout()->getBlock('product.price.render.default')->render(
\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE,
$product,
);
Magento then allows you to specify specific renderers and templates for each
product type.
• Default: Magento\Catalog\Pricing\Render\PriceBox
There is a specific order for loading these renderers (simple product type as
an example):
• simple/prices/final_price/render_class
• simple/default_render_class
• default/prices/final_price/render_class
• default/default_render_class
Pricing modifiers:
These modify and adjust prices that are visible (see \Magento\
Catalog\Model\ResourceModel\Product\Indexer\Price\
BasePriceModifier::modifyPrice). For example, if products are to be
hidden when they are out of stock, the CatalogInventory adjuster deletes
indexed prices for all out of stock items.
• Magento\CatalogInventory\Model\Indexer\
ProductPriceIndexFilter
• Magento\CatalogRule\Model\Indexer\ProductPriceIndexModifier
• Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\
CustomOptionPriceModifier
Practical experience:
You can also set breakpoints in each class that implements Magento\
Framework\Indexer\DimensionalIndexerInterface.
Price indexing is used to display an initial price, very fast, on lists of products.
There, the extra information does not matter.
Practical experience:
• Set breakpoints in the following, and browse the frontend:
• Magento\Framework\Pricing\Render::render
• Magento\Framework\Pricing\Render::renderAmount
• Magento\Framework\Pricing\Render\Amount::_toHtml
Tier prices
Tier prices are a way to provide discounts at quantity. Prices are configured for
combination websites and customer groups (or neither).
Special prices
Special prices is another way to say "sale." In Open Source, there are three
fields: Special Price, Special Price From (start date), and Special Price To (end
date). With Commerce, there is only Special Price as Scheduled Updates are
used to configure the start and end dates.
Custom options
Configurable adjustment
Catalog rules
Catalog rules adjust pricing before items are added to the cart. User-defined
attributes, categories and attribute sets are available for selection as a
condition. Action is the discount.
Cart rules
Cart rules adjust pricing after items are added to the cart. These are applied by
website and customer group. You can offer a coupon code.
If you wish to apply a cart rule by customer segment, you can specify this
as a condition. In addition to product attributes—total, weight, and shipping
destination are available as conditions.
One confusing point is that both conditions and actions have conditions.
The conditions tab determines whether or not to activate this price rule. The
actions tab defines which products to activate the rule for. A good example
is free shipping for specific products. Maybe some products are too heavy to
economically ship. You want to calculate live rates for the heavy products, but
exclude the tally for the light products.
Practical experience:
• Create a product with a custom option.
• Create a catalog rule. What conditions are present? How can these
conditions be modified?
• Create a cart price rule. Offer free shipping on just women’s jackets. Shipping
should calculate for all other products added to the cart.
Further Reading:
• Default price renderer: Magento\Catalog\Pricing\
Render\PriceBox
• vendor/magento/module-bundle/view/base/layout/
catalog_product_prices.xml
• Magento\Bundle\Pricing\Render\FinalPriceBox
Remember, calculations are used for the product page and beyond in the
customer journey. If these changes are not synchronized, customers will see
one price on the category page and another on the product page.
• Replicate the data structure from the $_POST for creating a price rule in
admin (see practice example).
• Feed this information into an empty price rule model and call loadPost.
• Save it.
Practical example:
Catalog rules are indexed. They are also staging enabled. As such, there can be
a very large number of rows in the catalogrule* tables. The indexers combine
the results in the catalogrule_product_price with the other indexed prices.
Further Reading:
• \Magento\CatalogRule\Model\Rule::calcProductPriceRule
Hierarchy
Custom attributes
Categories like other EAV entities use an attribute set and have attributes
assigned to them. When creating most attributes, category attributes included,
the attribute is automatically added to every applicable attribute set.
The big difference for category attributes is you need to create the
uiComponent XML to make the attribute visible on the category edit page.
Further Reading:
• Add a Category Attribute
Practical example:
• app/code/Chapter4/EAV/view/adminhtml/ui_component/category_form.xml
• Displays all products in child categories in this category. The benefit of this is
that product lists can be "drilled-down" by category or attribute. The
downside is that effectively managing the products displayed can become
unwieldy. This can contribute to poor user experience as products are not
curated and organized.
Further Reading:
• Anchor categories and position sorting explained
class="Magento\Catalog\Model\Indexer\Product\Price">
</indexer>
Practical experience:
• Create a PHPStorm PHP Script debug configuration to execute this
command: bin/magento indexer:reindex catalog_product_price.
The big picture is that the indexers are represented by the \Magento\
Indexer\Model\Indexer. There are three methods relating to updating index
rows: reindexAll, reindexRow and reindexList.
Important Note:
Running the indexer too often can impact the user experience.
Prices might be $0, for example, if the page is loaded while the
new price indexes are being inserted into the price index table.
Price Indexer
The price indexer determines the lowest price from all of the pricing sources
(price, special price, tier price, catalog price rules). This data is stored in
catalog_product_index_price.
The price indexer iterates through each product type’s indexerModel entry.
These instances should implement \Magento\Framework\Indexer\
DimensionalIndexerInterface (although there is backward compatibility
not to use this method).
What are dimensions? Dimensions are a way to execute the indexer in multiple
threads (processes) so indexing is run in parallel for each product type. Since
Magento 2.2.6, indexers can be executed in multiple threads separated by
Website and Customer Group (see \Magento\Catalog\Model\Indexer\
Product\Price\DimensionModeConfiguration). The PHP process is forked
into as many child processes as necessary per indexer.
This is especially helpful to ensure that large catalogs with many tiered prices
are indexed efficiently without taking down the entire site.
Further Reading:
• View indexer status
Practical experience:
• Set a breakpoint in each method in \Magento\Catalog\Model\Indexer\
Product\Price. How can you trigger each method (reindexAll,
reindexRow and reindexList)?
Inventory indexer
The inventory indexer determines the number of items in stock and whether or
not the item is available to be ordered.
product can specify its own stock indexer in the stockIndexerModel class.
This is utilized in bundle, configurable, and grouped products because their
stock status is dependent on the status of child products.
Indexing customizations are among the more difficult to make in Magento 2 and
to do it right. The majority of indexing operations aggregate data with complex
SQL queries. See \Magento\Catalog\Model\ResourceModel\Product\
Indexer\Price\DefaultPrice::getSelect for an example of such a query.
The indexer_state table stores the state of the indexers (aptly named). There
are three states: working, valid and invalid.
• Saving a product.
Practical experience:
• Set a breakpoint in \Magento\Catalog\Model\Indexer\Product\Price::executeRow.
• Save a product.
Changing the scope to, say, the store view would involve changing quite a
number of touchpoints. In regards to indexing, you would need to update all
pricing index tables (and the SQL queries to populate the data). This would likely
involve adding an extra join or query parameters.
The upside to customizing the index is that price lookups will be faster. The
downside is it takes more work to accomplish this.
Remember that indexing is useful only for pricing and information shown on
category and search pages (also in related/cross-sell/up-sell).
However, the question you might ask is: "How does the indexer only
index the active product?" The answer was discussed a few chapters
back. There is a Select renderer (\Magento\Staging\Model\Select\
FromRenderer::render) that adds the filters for the currently-active
product version.
Further Reading:
• Magento 2 Partial Index
Catalog triggers
See above.
• vendor/magento/module-elasticsearch
Read the DevDocs link below to understand how to update stopwords for a
given locale.
Further Reading:
• Install and configure Elasticsearch
Background:
Before we can understand the effects of imports at scale, we must look at what
happens in an import.
• The row data is converted to attribute values (via the source model, see
\
Magento\CatalogImportExport\Model\Import\Product\Type\
AbstractType::prepareAttributesWithDefaultValueForSave). Note
that commas in unintended places can break the import.
• \Magento\CatalogImportExport\Model\Import\
Product::saveProductEntity
• In before plugin, Staging sets the created_in and updated_in fields
to defaults.
• \Magento\CatalogImportExport\Model\Import\Product::_
saveProductCategories
• Products IDs are associated with categories in the
catalog_ category_ product table.
• \Magento\CatalogImportExport\Model\Import\Product::_
saveProductTierPrices
• Tier pricing is inserted into the database.
• \Magento\CatalogImportExport\Model\Import\Product\
MediaGalleryProcessor::saveMediaGallery
• Media gallery is inserted or updated.
• \Magento\CatalogImportExport\Model\Import\
Product::_
saveProductAttributes
• Attributes are inserted or updated en masse (not line-by-line).
After the above process happens, each product type can run custom actions
(for example, to automatically associate parent configurable products with
children). Running these actions after the initial import ensures that all SKUs are
present for their association.
There are several after plugins for the primary import method (\Magento\
ImportExport\Model\Import::importSource). This is an example where
the sort order for plugins is critical.
The reindex that runs with the every-one-minute cron will pick up these
changes and reindex them.
Frequent imports
The risk with frequent imports is largely dependent on their size and how
much data is being inserted. Frequent imports with few products would have
little impact on website performance. But, if there are many attributes, site
performance could suffer due to all of the attributes needing to be loaded.
Massive imports
This could be problematic no matter where the attributes are used in Magento.
For the import, Magento loads these attributes in from the database and stores
them in memory. There is the potential that you could run into memory limit
errors if there are too many attributes.
Save by model is the slowest approach. But, it is quite easy to do. However, the
indexing is called for each product that is created.
Custom SQL would be slightly faster than native import. The challenge is
ensuring that all data is present and properly inserted into the database. Also,
one must be careful that exceptions (for example, missing categories) are
handled appropriately.
• Putting commas into category names will break the category paths.
Quote-related objects
• Address: \Magento\Quote\Model\Quote\Address
• Payment: \Magento\Quote\Model\Quote\Payment
Total models
Totals come together to ultimately generate the grand total for an order.
Note: totals in Magento are comprised of two numbers, the total and the base
total (for example, grand total and base grand total). The base total is the
total in the base currency of the website (Store > Configuration > General >
Currency). The total is in the currency of the store view.
Totals are calculated per address. The amounts are tallied and assigned to the
quote object. Unless a store is using multi-address checkout or the customer
is checking out with a virtual cart, the total calculators are run twice: once for
billing and once for shipping.
Practical experience:
• Set a breakpoint in: \Magento\Quote\Model\Quote\
TotalsCollector::collect and add an item to the cart. Step through
each segment of this process.
• \Magento\Quote\Model\Quote\Address\Total\Grand::collect is
always called for each calculation.
• First, the final price is retrieved. Remember, the final price can take into
account a quantity (it does, in this instance). However, the price that the final
price returns is per-item.
• The quantity is multiplied by the final price. Quote Items can have a
custom_price set. If this is set, the final price is discarded, and the
custom price is used instead (\Magento\Quote\Model\Quote\Item\
AbstractItem::getCalculationPriceOriginal).
Practical experience:
The most common point for adding products to the cart is: \Magento\Quote\
Model\Quote::addProduct. This method takes the following actions:
\Magento\Wishlist\Controller\Index\Cart::execute
\Magento\Checkout\Controller\Cart\Add::execute
\Magento\Sales\Model\AdminOrder\Create::addProduct
Practical experience:
The Magento add controller provides a good example (although the wrapper for
the functionality is deprecated).
$quote->addProduct(/** … **/);
$quote->getShippingAddress()->setCollectShippingRates(true);
$quote->collectTotals();
$this->quoteRepository->save($this->getQuote());
It is important to call collectTotals to ensure that the quote has the latest
information regarding the items.
To override the price in the cart, use the custom_price data key. This overrides
the standard price calculation (using getFinalPrice).
Practical experience:
• \Chapter7\FreeGift\Plugin\AddFreeGiftToCart
• Note that this example is very basic and does not cover all scenarios.
• VAT
• Sales tax
• Fixed product tax (Magento\Weee). This allows you to configure flat product
taxes. In Europe, WEEE is a flat tax for each electronic sold, to cover recycling
it. In Store configuration, this is called Fixed Product Taxes. To enable for a
product, you must create a product attribute with the Fixed Product Tax
attribute type.
Further Reading:
• Magento_Weee module
Practical experience:
• Review Store Configuration > Sales > Tax, Checkout
Each item is compared. The method that compares the quote items is:
Magento\Quote\Model\Quote\Item\Compare::compare. If the
\
comparison returns false, a new item will be added. If the comparison returns
true, the quote item’s quantity is increased.
• The option codes (keys) are compared using the array_diff_key method.
If there is any variation, a merge is not possible.
• The values are also compared. Note that these comparisons are using non-
strict equality.
Practical experience:
• Set a breakpoint in \Magento\Quote\Model\Quote::merge.
• Add an item to your cart.
Practical experience:
• Set a breakpoint in
\Chapter7\FreeGift\Plugin\AddFreeGiftToCart::afterAddProduct
• Add an item to your cart.
Every time the cart’s totals are collected, these rules are re-applied for each
item (see \Magento\SalesRule\Model\RulesApplier::applyRules). The
conditions are filtered, and the actions are run. This can become very expensive.
Practical experience:
• Set a breakpoint in \Magento\SalesRule\Model\Quote\Discount::collect and
navigate to the cart page.
Practical experience:
Coupon codes are stored in the salesrule_coupon table. Create a CSV and
import this file directly into the table, and the coupon codes will be available.
Further Reading:
• How to Import Coupons Codes in Magento 2
$rule[] = [
];
First, is when you add another product to the cart (that has the same product
ID as already in the cart). \Magento\Quote\Model\Quote\Item::representProduct
handles this comparison. The product IDs are checked, then whether or not the
product is a child versus a parent, and finally the item’s options.
Second, is when carts are merged. There is very similar functionality (with the
exception of the parent/child item check) in \Magento\Quote\Model\Quote\
Item\Compare::compare.
Practical experience:
• Set a breakpoint in \Magento\Quote\Model\Quote\Item::representProduct and
add a similar item to the cart.
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_
Sales:etc/sales.xsd">
<section name="quote">
<group name="totals">
Total\Fee" sort_order="150"/>
</group>
</section>
</config>
Discounts and taxes are calculated on each item and not on the order’s subtotal
or other totals. As such, new totals do not affect these items.
Further Reading:
• How to Use Total Models in Magento 2
One option is to apply the discount to the shipping amount. Or, if you are like
most merchants, you will just give the customer free shipping.
Note that there are two types of endpoints for accessing the cart: /rest/V1/
carts/mine/items and /rest/V1/guest-carts/{cartId}. The difference
is that with a guest cart, you have to store and manage the cart ID. Initializing
a guest cart requires calling POST /rest/V1/guest-carts/ and storing the
quote_id. The challenge, then, is integrating the guest cart from the API with
the real checkout. The API does not set cookies, and you will likely need to
build a custom controller (which can set cookies) to assign the current session’s
quote ID to be the API’s quote ID.
Practical experience:
• Log into a Magento customer account.
• https://lc.commerce.site/chapter7quoteapi
• app/code/Chapter7/QuoteApi/view/frontend/web/js/items.js
Further Reading:
• Rest API's
Checkout steps
#2 Payment: choose payment method, set billing address, apply discount code,
gift card, and store credit.
Practical experience:
• Run through the checkout process yourself. It’s pretty simple, add items to
the cart and click the Proceed to Checkout button.
The default Magento checkout utilizes the REST API for persisting checkout
details. This enables easy extension points for customizations to add details.
However, many times it is safer to create a new REST API route than to modify
an existing route. With the ease of utilizing Javascript mixins, this is quite simple
to do.
Practical experience:
• Open Chrome Developer Tools and set a breakpoint in vendor/magento/
module-checkout/view/frontend/web/js/model/shipping-save-
processor/default.js (or /static/versionMYVERSION/frontend/
Magento/luma/en_US/Magento_Checkout/js/model/shipping-save-
processor.js), the saveShippingInformation. Select a shipping
method, and click Next.
Further Reading:
• Familiarize yourself with the REST API calls in the quote
namespace: Rest API's
Further Reading:
• Customize Checkout
• The submit button is located inside of the payment method form (example:
vendor/magento/module-offline-payments/view/frontend/web/
template/payment/checkmo.html).
• Clicking the Place Order button normally calls placeOrder in the above
Javascript class.
• Endpoints:
• Customer logged in: /rest/V1/carts/mine/payment-information:
Magento\Checkout\Api\PaymentInformationManagementInterface::
savePaymentInformationAndPlaceOrder
• Magento\Checkout\Api\GuestPaymentInformationManagementInterface::
savePaymentInformationAndPlaceOrder
Actions:
• Magento first checks to see if this is a B2B customer and if they have
permission to place orders.
• checkout_submit_before is called.
• \Magento\Quote\Model\QuoteManagement::submitQuote
• The order and order address objects are created from the quote.
• sales_model_service_quote_submit_before is triggered.
• \Magento\Sales\Model\Order::place
• sales_order_place_before
• sales_order_place_after
• Order is saved.
• sales_model_service_quote_submit_success is called
• This reindexes the catalog inventory index.
(\Magento\CatalogInventory\Observer\
ReindexQuoteInventoryObserver::execute)
• checkout_submit_all_after
• The checkout session contains the latest order and quote information.
Practical experience:
• Set a breakpoint in
\Magento\Checkout\Model\GuestPaymentInformationManagement::
savePaymentInformationAndPlaceOrder and
\Magento\Checkout\Model\PaymentInformationManagement::
savePaymentInformationAndPlaceOrder.
• Step through the checkout process for both guest and customer.
As you can see in the checkout action outline above, checkouts touch a majority
of the Magento system’s major features. This means that checkout is likely
the most difficult place to scale. One impact is the single point of failure: the
inventory catalog. This is improved in Magento 2.3.
Adding a step between the shipping and payment steps involves registering the
step with the correct sort order (see example in further reading).
Further Reading:
• Customize Checkout
• vendor/magento/module-checkout/view/frontend/
web/js/model/step-navigator.js
• vendor/magento/module-checkout/view/frontend/
web/js/view/shipping.js, initialize method
• A logged-in customer
• A payment method that is enabled for vault and instant purchase (Braintree
Credit Card, Braintree PayPal, and PayPal Payflow Pro)
Product page:
Confirmation window:
Success:
The biggest danger is that the customer is charged more than they are
expecting. Ensuring good customer communication is the most important part
of these considerations.
Practical experience:
• Sign up for a Braintree sandbox.
That is why the save payment information and place order are the
same method.
(see \
Magento\
Quote\Model\QuoteManagement::submitQuote).
This means that an error could cause inventory to be out of sync. The
good news is Magento thought of that and attached an observer to
the sales_model_service_quote_submit_failure event (see
\
Magento\
CatalogInventory\Observer\RevertQuoteInventoryObserver).
See:
• \Magento\CatalogInventory\Observer\
SubtractQuoteInventoryObserver::execute
• \Magento\CatalogInventory\Observer\
CheckoutAllSubmitAfterObserver::execute
Practical experience:
• Set a breakpoint in the above observers.
Note that you will likely want to reserve the order increment ID before sending
the quote to the queue. This is so that you can tell the user their order number.
You will also need to build notification mechanisms for if the payment method
is declined and a way for the customer to change the payment details.
The challenge is then knowing where to locate the orders. Looking up an order
is no longer selecting by ID or increment ID. Instead, you need to know more
information about the order.
Further Reading:
• Super-Scaling Magento
Payment methods pretty much fit into one of these categories. Gateway is
when the payment details (for example, credit card) is sent to Magento and then
on to the merchant. This can also apply to when the credit card is tokenized
(Braintree or Stripe) on the frontend and the authorization/capture happens
from Magento.
Offline is for payment types that are not connected to an external service.
Examples of this are Check/Money Order, Bank Transfer, Purchase Order and
Cash on Delivery (found in \Magento\OfflinePayments). These payment types
offer a Credit Memo but only for the entire order and not per invoice.
Instead of a central class that you create containing all information necessary
to complete a payment transaction, the central class is fixed and relies on
configuration to customize necessary functions (adapter pattern). To see this in
action, enable either our example payment method or a sandbox for true online
payment method and set a breakpoint in \Magento\Payment\Model\Method\
Adapter::getConfigData.
Each payment method in the gateway payment method framework must have:
• Command pool. This is a list of configured commands. See the next section.
Payment commands
• fetch_transaction_information
• order
• authorize
• capture
• refund
• cancel
• void
• acceptPayment
• denyPayment
• Locate the command pool type (or virtual type) for the payment method you
are modifying.
Further Reading:
• Adapter pattern
Practical experience:
• Run bin/magento Chapter7_PaymentMethods
• This checks the value handler pool to see if handler is registered for this
value. See SuperPaymentsValueHandlerPool for an example of how to
register a new value handler. You would create one for the active type.
Practical experience:
• Set a breakpoint in \Magento\Payment\Model\PaymentMethodList::getList and
go to the checkout page.
\Magento\Quote\Model\Quote\Address::requestShippingRates
assembles a shipping rate request. This object should contain all information
necessary to obtain shipping rates. Note the FreeMethodWeight is how much
weight should NOT be calculated in the rate request. For example, if a package
weight 2.5kgs, and the free method weight is 0.5kgs, rates will be retrieved for
shipping 2.0kgs.
A request can also limit the carrier. This is based on the limit_carrier data
key in a quote address object. This gives you an easy way to restrict options to
one choice.
• \Magento\Shipping\Model\Shipping::collectCarrierRates loads
shipping rates from a specific carrier (if the carrier is active).
Shipping method calculators are similar to the old way of payment methods.
There is one file that (usually) extends an abstract class and adds the lookup
functionality.
In our example, we have configured the FedEx carrier. An online carrier is one
that is connected to a third-party service or live rating. One potential impact to
understand is that when these services go down, it can bring a merchant’s site
down, too (we have had this happen).
Practical experience:
• Set a breakpoint in Magento\Quote\Model\Quote\Address\Total\
Shipping::collect and navigate to the checkout, enter your address, and
step through the collection process.
• You can also configure an online shipping method (in this example, FedEx),
and set a breakpoint in \Magento\Fedex\Model\Carrier::collectRates.
Evaluate the call stack and step through the rate collection process. Note
that the products ordered must have a weight.
Above gives a good overview for debugging shipping rates. Most often, the
shipping rate has been disabled (by a country exclusion, invalid credentials for
online carriers, or errors returned from online carriers).
Table rates provide one carrier name for a list of rates (you cannot specify both,
say, "Expedited Shipping" and "Standard Shipping"). Rates are uploaded per
website and cannot apply to the global scope. The available columns for table
rates are:
• Country
• Region/State
• Zip/Postal Code
• Shipping Price
\Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate::getRate
locates the rates for the shipping request.
• If desired, add validation rules for your shipping type (see vendor/magento/
module-offline-shipping/view/frontend/layout/checkout_cart_index.xml).
Practical experience:
• Enable and configure FedEx shipping in sandbox mode.
Files to create:
The DevDocs links below have more information. The goal of this section is to
provide a basic working knowledge so you can get into the code and execute it
yourself, giving a deeper understanding.
Practical example:
• See app/code/Queueing/BasicMessageQueue
Further Reading:
• AMQP 0-9-1 Model Explained
• RabbitMQ
• Message Queues
Use cases
Another way to look at message queues is similar to the Magento event system.
You dispatch (or, publish) a message to the event system. This event contains
data (or, operation). Magento routes the message to the appropriate observer
(or, consumer).
The Magento technical guidelines dictate that no changes are made to event
object data (there are a couple of exceptions). Because of this, most event
observers could become message queue consumers.
Examples:
• GDPR compliance in downloading customer data, then sending an email.
I believe that Magento’s admin interface will become more performant as the
message queues are adopted.
Practical experience:
• Create a new customer segment.
• Specify a criteria.
How it works:
For each segment that is valid for this customer, the customer is added
to the matched segments (\Magento\CustomerSegment\Model\
Customer::addCustomerToWebsiteSegments). This includes updating the
database table (magento_customersegment_customer) and adding the
segments to the session and the HTTP context.
Impact on performance
Because customer segments are re-calculated each time a customer (or visitor)
takes almost any cart or catalog-impacting action on the frontend, having many
segments can take a toll on performance.
Practical experience:
• Set a breakpoint in:
\Magento\CustomerSegment\Model\Customer::processEvent.
New entity:
$result[] = [
MyCondition::class,
];
return $result;
New attribute:
Store credits
You can make all credit memos be automatically returned through Store Credit
with Store Configuration > Customers > Store Credit > Refund Store Credit
Automatically.
Practical experience:
• Create a customer.
Reward points
Reward points create a loyalty program for customers. When customers take
specific actions, they get X number of points. They can then use these points to
purchase merchandise.
Reward points are configured in Store Configuration > Customers > Reward
Points. The exchange rate (points conversion to dollar value) is in Stores >
Reward Exchange Rates. Here, you can specify a website, Customer Group,
direction (Points to Currency and Currency to Points), and the exchange rate.
Additionally, you can use a cart price rule to add a fixed number of reward points
for a qualifying order.
The other important point to note is the configuration for Reward Points
Expiry Calculation. This default to static, which means that when the balance
increases, the expiration date is reset to the number of days specified. However,
setting to dynamic means that you can change the days to expiration, and it will
be figured into all future calculations.
Practical experience:
• Create a customer.
• Ensure reward points are enabled and configured to add points for specific
actions.
• Place an order.
Further Reading:
• Reward Points System on Magento 2 Enterprise Edition
RMA
The RMA module provides a streamlined process for approving and accepting
returns. This is found in Magento\Rma. Configuration is found in Store
Configuration > Sales > RMA Settings.
Returns have their own attribute type (Stores > Attributes > Returns).
If you need to add a new type of refund, you can easily add it to the
Resolution attribute.
Practical experience:
• Enable RMA in store configuration.
• Create a credit memo. This is not done automatically and requires the admin
to complete and submit a credit memo.
In the admin panel, RMA items follow a workflow of authorizing items, receiving
them (qty in the Returned field), and approving them. Note that the person
receiving the return must also change the status each time a QTY field
is updated.
If you need to make changes to the workflow, you can enjoy updating the old-
style Magento forms (not uiComponents). Depending on your experience, this
may or may not be a plus.
Further Reading:
• Configuring Returns
• Returns Attribute
Gift Cards
Gift cards are a product type. They are found in the \Magento\GiftCard
namespace. Gift cards are physical (shipped, thus shipping calculations are
applied) or virtual (via email). Configuration is found in Store Configuration >
Sales > Gift Cards.
Examples:
• https://lc.commerce.site/luma-mailed-gift-card
• https://lc.commerce.site/luma-virtual-gift-card
Purchased gift cards are visible under Marketing > Gift Card Accounts. By
default, the gift card account is created when the order is invoiced. You can
change this to create the gift card account when the order is placed (although,
the problem is that the purchasing credit card might authorize but fail to capture
and the customer now has a free gift card).
Each gift card has its own code that comes from a pool of generated codes
(magento_giftcardaccount_pool). These codes are configured in store
configuration. Once a card is associated with a gift card code, the status
column for the code (in the pool table) is switched to 1.
Further Reading:
• Creating a Gift Card
However, here are a couple of other locations that are helpful to tap into
RMA functionality:
• Don’t forget that RMAs have extension attributes. This is a great way to
export additional RMA information to an ERP.
• Cap Reward Points Balance At: prevents customer from obtaining too many
points.
• Reward Points Expire in (days): have reward points fall off after this number of
• Static: if this is set, changing Expire In (above) value only changes new
reward values.
• \Magento\Reward\Model\ResourceModel\Reward\History
• \Magento\Reward\Cron\ScheduledPointsExpiration::execute
(entry point for expiration customization)
• \Magento\Reward\Cron\ScheduledBalanceExpireNotification::execute (entry
point for expiration customization)
• \Magento\CustomerBalance\Model\Balance
• Customer segments
Target rules have two tabs: Products to Match and Products to Display. The
former selects products that these rules apply to. The latter selects products to
show on these products (in the list that is selected).
Further Reading:
• Creating a Related Product Rule
magento_
targetrule_index_related_product, magento_targetrule_
index_upsell, magento_targetrule_index_upsell_product). I am
unable to find where or how these tables are utilized, which leads me to believe
they may be abandoned.
Use cases
The primary use case to ease product associations. This is valuable because
one way to improve a website’s conversion rate and average order value is by
showing additional products that are pertinent or interesting.
$indexModel = $this->_getTargetRuleIndex()->setType(
$this->getProductListType()
// see \Magento\TargetRule\Model\Rule
)->setLimit(
$limit
)->setProduct(
$this->getProduct()
)->setExcludeProductIds(
$excludeProductIds
);
$indexModel->setLimit($limit);
return $indexModel->getProductIds();
Practical learning:
• Set breakpoints in the classes mentioned above.
Practical experience:
• Set a breakpoint in \Magento\TargetRule\Model\Indexer\
TargetRule\AbstractAction::_reindexByProductIds.
Both customers and EAV abstract provide filtering when utilizing the
extractValue method.
Practical experience:
• Set a breakpoint in \Magento\Customer\Model\Metadata\Form::extractData
• Create a customer.
Further Reading:
• Escape Methods
Practical experience:
• \Magento\Customer\Controller\Account\CreatePost::execute
Ensure that all output (whether loaded from the database or direct visitor input)
is escaped. Even if an admin is in control of the output (for example, products),
don’t trust it (admin accounts are cracked).
Further Reading:
• Cross site scripting
CSRF tokens
CSRF tokens ensure that the page was linked to from another validated page.
The way it does this is use a session token that creates a hash that is utilized on
the next page load. When you navigate to the linked page, the hash is verified
against the session.
There are two Magento features built for this: backend URL keys (secret key) and
form keys. These will be discussed in more detail later.
This effectively eliminates someone emailing you a link to your website that you
click, and it does something malicious (if you are logged in).
Stored XSS
This is when a user saves malicious details into the database (using even
legitimate means like creating a customer account or placing an order). While it
is not good to store malicious code in the database, the double-trouble comes
when rendering that code. In the case of the problem that SUPEE-7405 fixed
(Magento 1), people could create an email address with Javascript code.
Further Reading:
• Magento CSFR Protection
This is that really long string of characters that is appended to each admin URL:
https://lc.commerce.site/index.php/admin_dev/admin/index/index/
key/5010facb5b81eba2e80c2daf41c5b7c3e7b94b5eba9314fd5265e176d7cd51cc/
Security configuration
Practical experience:
• Familiarize yourself with Magento security configurations.
Privilege escalation is when a user is able to access something that they are not
allowed to do but those with more permissions are.
Preventing this involves carefully writing code and keeping the Magento
application up to date.
Example:
• https://magento.com/security/patches/magento-2.2.7-and-2.1.16-security-
update, see PRODSECBUG-2153, PRODSECBUG-2063, PRODSECBUG-2126,
PRODSECBUG-2152, and MAGETWO-94370
<form method="POST">
</form>
Practical learning:
• Set a breakpoint in \Magento\Backend\Model\Url::getSecretKey.
Further Reading:
• Using a Custom Admin URL
Note:
I strongly recommend reading each link in this section. This
information should be reviewed periodically to ensure it is
at the forefront of your mind while customizing Magento
applications.
This happens when an attacker is able to get PHP executed. This could give the
attacker access to system secrets, allow them to install skimmer technology or
the ability to steal lists of customers.
Further Reading:
• Code Injection
This happens when an attacker is able to get a local (on the file system) or a
remote file executed in the production environment. Again, this allows them to
access almost anything they want.
One way this can happen is if a code or class path is stored in the database. For
example, most rules (catalog price rule, cart price rule, customer segments,
target rules) store class paths in the database.
What helps to mitigate this is that Magento validates classes once they have
been instantiated to ensure they implement the correct values. See
\
Magento\
CustomerSegment\Model\ConditionFactory::create.
Further Reading:
• Testing for Local File Inclusion
Session hijacking
This happens when an attacker gets the session ID from the frontend,
adminhtml, or PHPSESSID cookie.
Further Reading:
• Session Hijacking
SQL injection
This happens when an attacker finds an input that is not sanitized before an
operation happens on the database. Good developers escape input and use
parameters wherever possible.
// BAD:
// GOOD
$this->select()
->from('catalog_product_entity')
Not only is the latter easier to read, it is protected against SQL injections.
Unfortunately, I have seen modules with the former code.
Further Reading:
• SQL Injection
Insecure functions
The context here would be not using dangerous PHP functions. There is little to
no reason to ever use eval. Be careful when using system commands like exec
as this can give unfettered access to the production environment. If possible,
use the Magento wrappers for any file system commands as they not only have
error handling built-in but there is also some security enhancements.
Further Reading:
• Dangerous PHP Functions
Further Reading:
• Path Traversal
Further Reading:
• Writing Secure Code
Further Reading:
• HttpOnly
Why serve from pub/? This limits exposure to possibly sensitive information
should the server be misconfigured. I have come across a number of websites
where their var/ directory is publicly accessible. One of those websites had
multiple human-readable SQL data dumps present—not good.
Serving from the pub/ directory prevents this from even happening.
Further Reading:
• How to add alternative Http headers to Magento 2
Further Reading:
• Secure cron.php to run in a browser
• X-Frame-Options header
Magento recommends against using an entity manager for any new work:
vendor/magento/framework/EntityManager/EntityManager.php. As
such, we will not discuss this too deeply other than to provide pointers as to
where you can learn more.
The idea behind EntityManager is that you use configuration (in di.xml) to
assemble most of the functionality that is needed—and that this would simplify
the process of storing and managing data. Ultimately, EntityManager proved too
complex as the simple abstract resource model does the job quite nicely.
<type name="Magento\Framework\EntityManager\MetadataPool">
<arguments>
</item>
</item>
</argument>
</arguments>
</type>
Note that there are a number of events that are available for the EntityManager.
Load/Create/Delete/Update:
• entity_manager_[action]_before
• $entityType_[action]_after
• entity_manager_[action]_after
Loading:
First, the metadata is loaded. This object contains information about the entity
to be saved, including references to additional tables (for example in the CMS
page entity, sequence_cms_page). Second, the hydrator is loaded. This takes
raw data that has been loaded from the database and returns a hydrated entity
model. The data is loaded from the database, then hydrated (\Magento\
Framework\EntityManager\Operation\Read\ReadMain::execute). All
linked fields are then loaded, and the data is merged. This happens for both
extensions (\Magento\Framework\EntityManager\Operation\Read\
ReadExtensions) and attributes (\Magento\Framework\EntityManager\
Operation\Read\ReadAttributes).
Saving:
Similar to loading, the same classes are loaded. The before events are run,
then the data is saved: first, the row data, next the attribute data, finally, the
extension data.
Practical experience:
• Navigate to a CMS page in adminhtml.
• Refresh.
Further Reading:
• Entity Manager