Magento 2 Certified Professional Developer Guide
Magento 2 Certified Professional Developer Guide
ECOMMERCE AGENCY
https://upload.wikimedia.org/wikipedia/commons/8/87/MVVMPattern.png
1. Model
The Model contains the application’s business logic and depends on an associated
class—the ResourceModel - for database access. Models depend on service contracts
to disclose their functionality to other application layers.
2. View
The View is both structure and layout of what is seen on a screen - the actual HTML.
This is achieved in the PHTML files distributed with modules. Such files are associated
with each ViewModel in the Layout XML files, sometimes referred to as binders. The
layout files can as well assign JavaScript files to be used on the final page.
3. ViewModel
The ViewModel works together with the Model layer and exposes only necessary
information to the View layer. In Magento 2, this is handled by the module’s Block
classes. Note that this was usually part of the Controller role of an MVC system. On
MVVM, the controller is only responsible for handling the user flow, meaning that it
receives requests and either tells the system to render a view or to redirect the user to
another route.
http://devdocs.magento.com/common/images/archi_diagrams_layers_alt4.jpg
Magento 2 architecture consists of 4 layers:
1. Presentation Layer
2. Service layer
Service layer is the layer between presentation and domain layers. It executes service
contracts, which are implemented as PHP interfaces. Service contracts allow to add or
change the business logic resource model using the dependency injection file (di.xml).
The service layer is also used to provide API access (REST / SOAP or other modules).
The service interface is declared in the / API namespace of the module.
Data (entity) interface is declared inside / Api / Data. Data entities are the structures of
data passed to and returned from service interfaces.
3. Domain layer
The domain layer is responsible for business logic that does not contain information
about resources and the database. Also, the domain layer may include the
implementation of service contracts. Each data model at the domain layer level
depends on the resource model, which is responsible for the database access.
4. Persistence layer
The persistence layer describes a resource model that is responsible for retrieving and
modifying data in a database using CRUD requests.
It also implements additional features of business logic, such as data validation and the
implementation of database functions.
The whole Magento structure can be divided into the following types:
1. Magento root structure
2. Modules structure
3. Themes structure
To begin with, Magento has various areas that allow to determine configuration, view
files, etc. for a certain area. Adminhtml (applied to the administration panel) and
2) bin - there the executed Magento file is located that allows to manage the
website via CLI.
3) dev - the directory for Magento test scripts (for more information, follow the link
https://devdocs.magento.com/guides/v2.3/mtf/mtf_quickstart.html).
4) generated - contains the generated php files (Factories, Proxies, Interceptors, DI
configuration).
5) lib - used for Magento library files.
6) phpserver - contains the router file “router.php” for the php Built-in web server.
Allows to use Magento without a third-party web server, like nginx and apache.
Here is the example of php Built-in web server launch:
php -S 127.0.0.1:8082 -t ./pub/ ./phpserver/router.php
7) pub - the directory used to access Magento static files. It contains the following
directories:
a) errors - stores the error pages,
b) media - stores all media files (product, pages, etc.),
c) static - stores Magento themes generated files.
This directory can be specified as web root in nginx config or in apache config.
Numerous Magento directories contain “.htaccess” files (including root and pub), which
allow you to configure apache for a specific directory. Nginx does not support
Hereinafter <
module_dir> will be used to specify module root directory, for this directory
can be located both in app/code and in vendor.
Inside the module directory, the following directories and files are located:
The following directories and files are located in the directory together with the theme:
a) <ModuleVendor>_<ModuleName> - common template for override view module
files. For example, BelVG_CustomModule.
Inside the directories and files, the structure is similar to view/<area>/ folder
inside the module (for example,
app/code/BelVG/CustomModule/view/frontend).
b) etc - contains theme configuration files.
c) media - contains theme media files.
d) web - contains theme css/js/fonts files.
e) Theme.xml file - a mandatory file with the theme configurations, like the name of
the theme and its parent.
f) requirejs-config.js - contains RequireJS config.
g) registration.php - a mandatory file that registers a theme in Magento.
h) composer.json - contains composer package configuration.
To find the files responsible for certain functionality, search among di.xml files
(Dependency Injection configuration). Another way is to search in a module or theme
layout files, if you need to find certain blocks. The hints, enabled in the store settings
(Stores -> Configuration -> Advanced -> Developer), can also prove helpful for this type
of search.
To search for the model functionality, use the full class or interface name (including
namespace). Conduct the file search, specifying the full name of the class or the
interface (without leading backlash).
In case of SSH access, you can use `grep` to search files with particular content:
grep -r 'String that you are looking' [path/to/search]
Example (search files with content "Catalog\Model\ProductRepository" in
vendor/magento/module-catalog folder):
grep -r 'Catalog\\Model\\ProductRepository' vendor/
In case you know the file name, you can use `find`:
find [path/to/search] -name 'FileNameMask'
Example (search all webapi.xml files in `vendor` folder):
find vendor/ -name webapi.xml
One can modify the configuration from the admin panel only when it is stored in
core_config_data and is not overridden in pp/etc/config.php, app/etc/env.php or via
environment variables. In case it is overridden, it is disabled at the admin panel:
Magento loads different areas and files separately; it also has different file loaders for
each file type.
In Magento 2, XML configuration files have areas: global, frontend and adminhtml,
crontab, graphql, webapi_rest, webapi_soap. You can find a list of them at
Magento\Framework\App\AreaList class, defined via di.xml. Certain xml files can be
specified for each area separately and some may not. For instance, event.xml file can
be specified for each area (global, frontend, adminhtml, etc.), while module.xml can be
specified only for global.
If config file is located at the module etc directory, its values are located in the global
area. To specify configuration area, place the config file into etc/<area> folder. This is a
new concept, introduced in Magento 2. Previously, in the first version of Magento, the
visibility area was defined by a separate branch in XML file. This introduction allows to
load configurations for various visibility areas separately. If the same parameters are
specified for global and non-global areas (for instance, frontend or adminhtml), they will
be merged and loaded together. The parameters, specified in non-global area, or located
in etc/<area> folder, have the priority.
1. System level configurations upload. Loading of the files, necessary for Magento
2 launch (like config.php).
2. Global area configurations upload. Loading of the files, located in app/etc/
Magento 2 directory, such as di.xml, as well as files that relate to the global area
and are directly located in modules’ etc/ folders.
Configuration files are merged according to their full xPaths. Specific attributes are
defined in the $ idAttributes array as identifiers. When two files are merged, they contain
all the nodes and values from the original files. The second XML file either adds or
replaces the nodes of the first XML file.
Each XML file type is validated by the corresponding XSD validation scheme. All
validation schemes are in etc / directories of the module. The most important schemes
are located in the Magento framework-e (vendor / magento / framework); for example,
XSD for acl.xml is located in vendor/magento/framework/Acl/etc/acl.xsd directory.
Magento 2 has two types of validation for XML configuration files: before and after the
merge. The schemes can be the same or differ from each other.
● XML file
● XSD schema
● Config PHP file
● Config reader
● Schema locator
● Converter
Not all those elements are necessary. Instead of creating them, one can use virtualType
in di.xml and create only the following elements:
● XML file
● XSD schema
● Converter
1. We begin with XSD file creation. Before the merger, Magento_Catalog uses
product_types.xsd validation scheme and product_types_merged.xsd scheme for
the merged XML file.
2. Create the configuration PHP file for access to the file data; in our case, it will be
Config.php. To provide access to the product_types.xml file data, it implements
the Magento\Catalog\Model\ProductType\ConfigInterface interface and realizes
all its methods.
3. We should get reader class in Config.php in the constructor. In our case, it’s
Magento\Catalog\Model\ProductType\Config\Reader. This is a small class with
a certain $_idAttributes attribute. In $fileName variable at the constructor we
define the XML file name.
4. Magento\Catalog\Model\ProductType\Config\SchemaLocator implements two
methods: getSchema and getPerFileSchema return the path to merged XSD and
common XSD files. In the constructor, we define these paths in $_schema and
$_perFileSchema attributes.
5. Convertor class creation. In our case:
Magento\Catalog\Model\ProductType\Config\Converter it implements
Dependency inversion principle claims that high-level classes should use low level
objects’ abstractions instead of working with them directly.
Object Manager
Magento 2 applies dependency injection for the functionality, which was offered by
Mage class in Magento 1.
namespace Magento\Backend\Model\Menu;
class Builder
{
public function __construct(
* If shared=false attribute is specified for a certain type in di.xml, then a new object will
be created at the future requests.
A centralized process creating object instances decreases code coherency and lowers
the incompatibility risk in case the object realization changes.
<config>
<preference for="Magento\Core\Model\UrlInterface"
type="Magento\Core\Model\Url" />
</config>
A similar approach can be used to substitute not only the interfaces, but the classes
themselves.
<config>
<preference for="Magento\Core\Model\Url"
type="Vendor\Module\Model\NewUrl" />
</config>
Virtual Types
Virtual Type allows to modify any dependencies arguments and therefore modify class
behavior without changing the operation of other classes that depend on the original.
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/et
c/config.xsd">
<virtualType name="moduleConfig"
type="Magento\Core\Model\Config">
<arguments>
ObjectManager should never be called directly, because Magento Framework makes the
call automatically. Factory or proxy classes, as well as unit texts or static and magic
methods (wakeup, sleep) can be considered exceptions, for they are majorly generated
by the framework automatically.
Magento 2 code contains direct ObjectManager calls, which exist only for backward
compatibility and should not be used as an example.
Dependencies compilation
bin/magento setup:di:compile
Magento 2 applies a specific utilita for compiling dependencies of all classes. The
utilita creates a file that contains the dependencies of all objects for ObjectManager,
based on constructor arguments with the help of php reflection features. Such service
classes as factories, proxies and plugins, are generated as well.
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/et
c/config.xsd">
<type name="{ObservedType}">
<plugin name="{PluginName}" type="{PluginClassName}"
sortOrder="1" disabled="false"/>
</type>
</config>
, where:
{ObservedType}- class name, the method that must be altered
{PluginName}- plugin name
{PluginClassName}- plugin class name
sortOrder- plugin call order
disabled- ability to disable the plugin
Plugin Methods
Before Methods
Before methods allow to modify target class method arguments before the method is
called.
Namespace Vendor\Module\Plugin;
class Plugin
{
public function beforeMethodName(\Vendor\Module\Model\TargetClass
$subject, $arg1, $arg2, $arg3 = null)
{
return [$arg1, $arg2, $arg3];
}
The name of the plugin method is concatenated ‘before’ and the method name, which
arguments must be altered. The first argument of this method is the class, the method
of which is called. The rest of the arguments correspond to the called methods
arguments, including default values. It is also possible to use “...” token to get all the
arguments
(https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list.
new). Example:
public
function beforeMyMethod($subject, ...$args)
{
return $args;
}
Before methods should return the arguments array, if they need to be overridden, or null,
if there is no need for that.
After Methods
After methods allow to modify the result of the target method.
Namespace Vendor\Module\Plugin;
class Plugin
{
public function afterMethodName(\Vendor\Module\Model\TargetClass
$subject, $result)
{
return $result;
}
}
The first argument, the same as before methods, is target class instance, the second is
the returned value of the original method, while the third and further are the original
method arguments.
class Plugin
{
public function aroundMethodName(\Vendor\Module\Model\TargetClass
$subject, callable $proceed, $arg1, $arg2, $arg3)
{
$result = $proceed($arg1, $arg2, $arg3);
return $result;
}
}
Around methods allow to execute the code before and after the target method in one
place.
$proceed argument is PHP closure, that in its turn calls the target method.
Such methods allow to completely substitute the target method.
Plugin Sorting
Plugin sortOrder parameter allows to identify what the order plugin methods will be
called in case multiple plugins are observing the same method.
sort order 10 20 30
1. Plugin1::beforeMethod()
2. Plugin2::beforeMethod()
The main purpose of plugins is to modify the certain method input, output or execution.
In case the data is not modified (for instance, when order details are sent to the 3rd
party ERP), then it is recommended to apply observers instead of plugins.
Events are part of the Event-Observer pattern. This design pattern is characterized by
objects (subjects) and their list of dependents (observers). It is a very common
programming concept that works well to decouple the observed code from the
observers. Observer is the class that implements
Magento\Framework\Event\ObserverInterface interface.
According to
https://devdocs.magento.com/guides/v2.3/coding-standards/technical-guidelines.html
:
All values (including objects) passed to an event MUST NOT be modified in the event
observer. Instead, plugins SHOULD BE used for modifying the input or output of a
function.
Therefore, if there is a need to modify the input data, use plugins instead of events.
In instance attribute we declare observer class name. This class has to implement
Magento\Framework\Event\ObserverInterface::execute(Observer $observer) method.
The $observer object has an $event object (available through $observer->getEvent()),
which contains the event’s parameters.
namespace Namespace\Modulename\Observer;
use Magento\Framework\Event\ObserverInterface;
Group element determines to which group cron jobs should be tied. Group is declared in
cron_groups.xml file and contains group configurations. The events inside the group
have a general queue, while several groups can be launched simultaneously. Example:
Job element contains name (name of the job), instance (job class name) and method
(job method name in the class) attributes. Also, job contains schedule element
(http://www.nncron.ru/help/EN/working/cron-format.htm) or config_path
(configuration path to the schedule value).
Controllers:
controller_action_predispatch_[ROUTE_NAME]
controller_action_predispatch_[FULL_ACTION_NAME]
controller_action_postdispatch_[ROUTE_NAME]
controller_action_postdispatch_[FULL_ACTION_NAME]
controller_action_layout_render_before_[FULL_ACTION_NAME]
1. Login into your Magento 2 server via SSH as Magento file system owner
$ ssh magento_user@server.com
For example, list command will put out a list of available actions:
$ bin/magento list
● Exceptions are recorded only in log files and not put out at the screen
● Static files are put out from cache (the files are generated in advance with
$ php bin/magento setup:static-content:deploy command)
● Automatic code compilation is disabled (it is executed in advance with $
php bin/magento setup:di:compile command)
● X-Magento-* HTTP response headers are hidden.
where:
● {mode} is the required mode (default, developer or production)
● --skip-compilation - is an optional parameter that allows to skip code compilation
after the deployment mode is changed.
When the deployment mode is changed, the var/cache folders will be cleared, except for
generated/metadata, generated/code, var/view_preprocessed, pub/static files
.htaccess. To avoid this, apply --skip-compilation flag.
Out-of-the-box Magento supports two FPC types: built-in and Varnish. Varnish is
recommended to be installed and applied for production use. Before the page gets into
FPC, all its personalized content is deleted; this also applies for both built-in FPC and
Varnish.
<referenceContainer name="content">
<block class="Magento\Checkout\Block\Onepage\Success"
name="checkout.success" template="success.phtml" cacheable="false"/>
<block class="Magento\Checkout\Block\Registration"
name="checkout.registration" template="registration.phtml"
cacheable="false"/>
</referenceContainer>
Attribute cacheable=false makes page with this block uncacheable by FPC. But be
careful with this parameter, because if this block is located in all pages, then all these
pages will not be cached by FPC.
Second.
You could use ajax for this purpose. Commonly, Magento has two types of content:
● Public. Public content is stored server side in your reverse proxy cache storage
(e.g., file system, database, Redis, or Varnish) and is available to multiple
customers. Examples of public content include header, footer, and category
listing.
● Private. Private content is stored client side (e.g., browser) and is specific to an
individual customer. Examples of private content include shopping cart, message
and customer. The data is loaded from the server and saved to localStorage of
user’s browser. In case the sections are invalidate, they are loaded from the
server repeatedly. This is realized by customer data JS module.
The strategy of deferring private content is perhaps best demonstrated by the following
example. Starting in Magento_Theme::view/frontend/templates/html/header.phtml, we
see the following:
Magento has different FPC for each group of customers, applying X-Magento-Vary
Cookie for this. If the page with the specified URL and X-Magento-Vary Cookie is stored
in FPC, then it is put out to the user, saving loading time. X-Magento-Vary Cookie value
is generated using Magento\Framework\App\Http\Context class. In order to add a
custom FPC division (for instance, according to age: < 18 and >= 18), one must call
\Magento\Framework\App\Http\Context::setValue($name, $value, $default method.
● Clean manually
○ File cache: “rm -rf var/cache/*”, “rm -rf var/page_cache/*”
○ Redis: “redis-cli flushall”
○ Restart services (Varnish, Redis)
The cache tags are generated at block level, with each block class implementing the
IdentityInterface which means they must implement a getIdentities method, which must
return a unique identifier. For example:
...
namespace Magento\Cms\Block;
...
use Magento\Framework\View\Element\AbstractBlock;
use Magento\Framework\DataObject\IdentityInterface;
...
class Page extends AbstractBlock implements IdentityInterface
{
...
public function getIdentities()
{
return [\Magento\Cms\Model\Page::CACHE_TAG . '_' .
$this->getPage()->getId()];
}
...
When the front controller response is ready, the FPC combines all the block tags from
the layout, and then adds them to the response in a X-Magento-Tags custom HTTP
header. The different FPC options then handle the header differently. Varnish stores the
header along with the rest of the page when it is cached, so no additional work is
required. The built-in option however needs some additional code to pull the tags back
out of the X-Magento-Tags header so that they can be associated with the response
when it is stored in the configured storage (e.g. Redis).
In this page you can clean/enable/disable cache by cache types or full cache. Also you
can clean cache from console:
bin/magento cache:clean- clean cache,
bin/magento cache:clean <cache_type>- clean cache only for <cache_type>
bin/magento cache:status- you could see statuses and types of cache
bin/magento cache:enable<cache_type> - enable cache
bin/magento cache:flush- flush cache
Step 2
Step 3
Step 4
Then, createApplication() of the Bootstrap object is called.
$app = $bootstrap->createApplication(\Magento\Framework\App\Http::class);
In this method, an instance of the Magento\Framework\App\Http class is created with
the help of ObjectManager and is returned into index.php.
Step 5
Then, call run() method of the Bootstrap object to launch the application that will call
launch() method of the application object.
$bootstrap->run($app);
public
function run(AppInterface $application)
{
try {
try {
\Magento\Framework\Profiler::start('magento');
Step 6
An instance of Http object performs the initial routing; as a result, it determines the area
from URL and sets in $this->_state->setAreaCode($areaCode). After the area is set, the
required configuration for that area is loaded.
Then, an object of \Magento\Framework\App\FrontController class is created and its
method - dispatch($this->_request) - is called, to which request is passed.
public
function launch()
{
$areaCode =
$this->_areaList->getCodeByFrontName($this->_request->getFrontName())
;
$this->_state->setAreaCode($areaCode);
$this->_objectManager->configure($this->_configLoader->load($areaCode
));
/** @var \Magento\Framework\App\FrontControllerInterface
$frontController */
$frontController =
$this->_eventManager->dispatch('controller_front_send_response_before
', $eventParams);
return $this->_response;
}
Step 7
In dispatch() method of the FrontCotroller class the current router and the current
action controller are defined. Then, dispatch() method is called from action controller.
public
function dispatch(RequestInterface $request)
{
\Magento\Framework\Profiler::start('routers_match');
$routingCycleCounter = 0;
$result = null;
while (!$request->isDispatched() && $routingCycleCounter++ < 100)
{
/** @var \Magento\Framework\App\RouterInterface $router */
Step 8
Dispatch() method is implemented in Magento\Framework\App\Action\Action.php.
When we create custom actions, they are inherited from this class.
Action controller returns the object that realizes ResultInterface via execute() method.
$result = null;
if ($request->isDispatched() && !$this->_actionFlag->get('',
self::FLAG_NO_DISPATCH)) {
\Magento\Framework\Profiler::start('action_body');
$result = $this->execute();
\Magento\Framework\Profiler::start('postdispatch');
if (!$this->_actionFlag->get('',
self::FLAG_NO_POST_DISPATCH)) {
$this->_eventManager->dispatch(
'controller_action_postdispatch_' .
$request->getFullActionName(),
$eventParameters
);
$this->_eventManager->dispatch(
'controller_action_postdispatch_' .
$request->getRouteName(),
$eventParameters
);
Step 9
FrontController returns ResultInterface into Application Instance, which puts out a
response.
public
function launch()
{
$areaCode =
$this->_areaList->getCodeByFrontName($this->_request->getFrontName())
;
$this->_state->setAreaCode($areaCode);
$this->_objectManager->configure($this->_configLoader->load($areaCode
));
/** @var \Magento\Framework\App\FrontControllerInterface
$frontController */
$frontController =
$this->_objectManager->get(\Magento\Framework\App\FrontControllerInte
rface::class);
$result = $frontController->dispatch($this->_request);
// TODO: Temporary solution until all controllers return
ResultInterface (MAGETWO-28359)
if ($result instanceof ResultInterface) {
$this->registry->register('use_page_cache_plugin', true,
true);
$result->renderResult($this->_response);
} elseif ($result instanceof HttpInterface) {
Developer mode
In Developer mode, static view files are generated every time they are requested. The
symlinks of them are written to the pub/static directory. If you will change content of JS
file, it will be updated in pub/static too because of symlink.
Uncaught exceptions are displayed in the browser instead of being logged. An exception
is thrown whenever an event subscriber cannot be invoked.
Magento 2 validates XML files using schemas in this mode.
Use the Developer mode while developing customizations or extensions. The main
benefit of this mode is that error messages are visible to you. It should not be used in
production because it impacts the performance.
Default mode
Default mode is how the Magento software operates if no other mode is specified.
In this mode, errors are logged to the files in var/reports and are never shown to the
user. Static view files are materialized on the fly and then cached.
In contrast to the developer mode, view file changes are not visible until the generated
static view files are cleared.Default mode is not optimized for a production environment,
primarily because of the adverse performance impact of static files being materialized
on the fly rather than generating and deploying them beforehand. In other words,
creating static files on the fly and caching them has a greater performance impact than
generating them using the static file creation command line tool.
Front Controller
Routing in Magento 2 is based on Front Controller Pattern. Front Controller is a design
pattern, in which one component is responsible for processing all the incoming
requests, redirecting them to the corresponding components, further results processing
and returning the result to the browser.
FrontController iterates via the available routers and, if the action responsible for the
current URL is successfully found, calls the
\Magento\Framework\App\Action\AbstractAction::dispatch method.
Routers
All routers in Magento 2 should implement \Magento\Framework\App\RouterInterface
interface and define \Magento\Framework\App\RouterInterface::match method. This
In order to find out which module, controller and action are applied now, call the
following $request methods (\Magento\Framework\App\Request\Http):
For instance, to define the current module, controller and action, temporarily add at the
end of index.php file (or pub/index.php) the following code:
$request =
\Magento\Framework\App\ObjectManager::getInstance()->get('\Magento\Fr
amework\App\Request\Http');
var_dump($request->getControllerModule(),
$request->getControllerName(), $request->getActionName());
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/et
c/config.xsd">
<type name="Magento\Framework\App\RouterList">
<arguments>
<argument name="routerList" xsi:type="array">
<item name="customrouter" xsi:type="array">
<item name="class"
xsi:type="string">Vendor\Module\Controller\CustomRouter</item>
<item name="disable"
xsi:type="boolean">false</item>
<item name="sortOrder" xsi:type="string">22</item>
</item>
</argument>
</arguments>
</type>
</config>
<?php
namespace Vendor\Module\Controller;
use Magento\Framework\App\ActionFactory;
use Magento\Framework\App\RouterInterface;
public
function match(\Magento\Framework\App\RequestInterface
$request)
{
$identifier = trim($request->getPathInfo(), '/');
if (strpos($identifier, 'customrouter-test') !== false) {
$request->setPathInfo('/customrouter/index/index/');
// or
$request->setModuleName('customrouter');
$request->setControllerName('index');
$request->setActionName('index');
$request->setControllerModule('Module_Vendor');
$request->setRouteName('customrouter');
$request->setParams(['param1' => 'value']);
} else {
return false;
}
eturn
r
$this->actionFactory->create('Magento\Framework\App\Action\Forward');
// or
return
$this->actionFactory->create('Vendor\Module\Controller\Index\Index');
}
}
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.x
sd">
<router id="standard">
<route id="customrouter" frontName="customrouter">
<module name="Vendor_Module" />
</route>
</router>
</config>
Specify standard in Router id: for frontend, for adminhtml - admin. In this file you can
also specify the configuration for another router. This configuration is not passed into
the router class automatically; instead, it needs to be loaded with
\Magento\Framework\App\Route\Config\Reader class. Base Router
(\Magento\Framework\App\Router\Base) applies
\Magento\Framework\App\Route\Config class for loading. This class loads the
configuration only for default router of an area (default router is set via di.xml) and
stores it in cache.
Route id is the id of the route. It is applied, for instance, for layout files naming
({route_id}_{controller}_{action}.xml). To get the ID, use $request->getRouteName().
The method searches for a suitable URL Rewrite in url_rewrites table, based on request
path and current store ID. In case the search is successful,
\Magento\UrlRewrite\Service\V1\Data\UrlRewrite object is returned.
<event name="catalog_product_save_after">
<observer name="process_url_rewrite_saving"
instance="Magento\CatalogUrlRewrite\Observer\ProductProcessUrlRewrite
SavingObserver"/>
</event>
public
function execute(\Magento\Framework\Event\Observer $observer)
{
/**
@var Product $product */
$product = $observer->getEvent()->getProduct();
if ($product->dataHasChangedFor('url_key')
|| $product->getIsChangedCategories()
|| $product->getIsChangedWebsites()
|| $product->dataHasChangedFor('visibility')
) {
$this->urlPersist->deleteByData([
UrlRewrite::ENTITY_ID => $product->getId(),
UrlRewrite::ENTITY_TYPE =>
ProductUrlRewriteGenerator::ENTITY_TYPE,
UrlRewrite::REDIRECT_TYPE => 0,
UrlRewrite::STORE_ID => $product->getStoreId()
]);
if ($product->isVisibleInSiteVisibility()) {
$this->urlPersist->replace($this->productUrlRewriteGenerator->generat
If certain elements have changed, then the current URL rewrites for this product are
deleted using \Magento\UrlRewrite\Model\Storage\DbStorage::deleteByData method.
Afterward, if product settings allow its display,
\Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::generate method is
called.
$storeId = $product->getStoreId();
$productCategories = $product->getCategoryCollection()
->addAttributeToSelect('url_key')
->addAttributeToSelect('url_path');
$urls = $this->isGlobalScope($storeId)
? $this->generateForGlobalScope($productCategories, $product,
$rootCategoryId)
: $this->generateForSpecificStoreView($storeId,
$productCategories, $product, $rootCategoryId);
return $urls;
}
public
function generateForGlobalScope($productCategories, Product
$product, $rootCategoryId = null)
{
$productId = $product->getEntityId();
$mergeDataProvider = clone $this->mergeDataProviderPrototype;
!$this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore(
$id,
$productId,
Product::ENTITY
)) {
$mergeDataProvider->merge(
$this->generateForSpecificStoreView($id,
$productCategories, $product, $rootCategoryId)
return $mergeDataProvider->getData();
}
public
function generateForSpecificStoreView($storeId,
$productCategories, Product $product, $rootCategoryId = null)
{
$mergeDataProvider = clone $this->mergeDataProviderPrototype;
$categories = [];
foreach ($productCategories as $category) {
if (!$this->isCategoryProperForGenerating($category,
$storeId)) {
continue;
}
// category should be loaded per appropriate store if category's URL key has been
changed
$categories[] = $this->getCategoryWithOverriddenUrlKey($storeId,
$category);
}
$productCategories =
$this->objectRegistryFactory->create(['entities' => $categories]);
$mergeDataProvider->merge(
\Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator::generateForSpeci
ficStoreView method for each of the product categories receives a specific url_key for
the current core. Then, with the help of \Magento\UrlRewrite\Model\MergeDataProvider
instance, it merges the results of several calls:
a. \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator::gen
erate - creates URL Rewrite that does not contain categories.
As a result, all the methods are united in a single array and are returned to
\Magento\UrlRewrite\Model\Storage\AbstractStorage::replace method, responsible for
URL Rewrites persistence.
Controllers
Controllers in Magento 2 differ from the typical MVC applications’ controllers. In MVC
applications, controller is a class, while action is the method of this class. In Magento 2,
controller is a folder (or php namespace), while action is a class, located in this folder
(in this php namespace). Execute m ethod of the action returns the result object and
occasionally processes input POST data. All actions inherit
\Magento\Framework\App\Action\Action class.
Search and initialization of the needed action is performed in router. For instance, in
Base Router:
public
function match(\Magento\Framework\App\RequestInterface
$request)
{
$params = $this->parseRequest($request);
protected function
matchAction(\Magento\Framework\App\RequestInterface $request, array
$params)
{
$moduleFrontName = $this->matchModuleFrontName($request,
$params['moduleFrontName']);
if (
empty($moduleFrontName)) {
return null;
}
/**
* Searching router args by module name from route using it as key
*/
$modules =
$this->_routeConfig->getModulesByFrontName($moduleFrontName);
if (
empty($modules) === true) {
return null;
}
/**
* Going through modules to find appropriate controller
*/
$currentModuleName = null;
$actionPath = null;
$action = null;
$actionInstance = null;
$actionPath = $this->matchActionPath($request,
$params['actionPath']);
$action = $request->getActionName() ?: ($params['actionName'] ?:
$actionClassName = $this->actionList->get($moduleName,
$this->pathPrefix, $actionPath, $action);
if (!$actionClassName || !is_subclass_of($actionClassName,
$this->actionInterface)) {
continue;
}
$actionInstance =
$this->actionFactory->create($actionClassName);
break;
}
if (
null == $actionInstance) {
$actionInstance =
$this->getNotFoundAction($currentModuleName);
if ($actionInstance === null) {
return null;
}
$action = 'noroute';
}
$request->setRouteName($this->_routeConfig->getRouteByFrontName($modu
leFrontName));
if (
isset($params['variables'])) {
Each URL segment contains the information for the required action search. The
segments in URL can be presented the following way:
{routeFrontName}/{controllerName}/{actionName}
Where:
{routeFrontName}- route front name as set in routes.xml file
{controllerName}- name of the controller
{actionName}- name of the action
Forward Result
public
function __construct(
Magento\Framework\Controller\Result\Forward\Factory
$resultForwardFactory
) {
$this->resultForwardFactory = $resultForwardFactory;
}
public
function execute()
{
Redirect Result
Redirect Result (\Magento\Framework\Controller\Result\Redirect) - allows to redirect
the browser to another URL.
public
function __construct(
Magento\Framework\Controller\Result\Redirect\Factory
$resultRedirectFactory
) {
$this->resultRedirectFactory = $resultRedirectFactory;
}
public
function execute()
{
$result = $this->resultRedirectFactory->create();
$result->setPath('*/*/index');
return $result;
}
Responses
Action in Magento 2 can return several response types depending on its purpose and
desired result.
Page Result
Page Result (\Magento\Framework\View\Result\Page) is the most common response
type. Returning the object, Magento calls its renderResult method that performs page
rendering based on the corresponding XML layout handle.
public
function execute()
{
return $this->pageResultFactory->create();
}
JSON Result
JSON Result (\Magento\Framework\Controller\Result\Json) - allows to return the
response in JSON format. Can be applied in API or AJAX requests.
public
function execute()
{
$result = $this->jsonResultFactory();
In web applications, such as Magento, routing is the act of providing data from a URL
request to the appropriate class for processing. Magento routing uses the following
flow:
The need to create a new router or modify the current one may appear when one needs
to change the default Magento 2 URLs (processed by Base Router) for the custom ones,
for instance, /belvg/index/mypage on /mypage.
This is how an out-of-the-box Magento 404 page looks. However, some store owners
wish to have a custom 404 page, and in Magento 2, this is not a problem, for the
platform provides a very flexible tool for modifying this element.
To modify the “Not Found“ (404 error) page, log in to the admin panel and navigate the
following path: Store -> Configuration-> General -> Web -> Default pages -> CMS No
Route Page to check what CMS page is set in the configurations.
Open 404 page settings and modify the page the way you find necessary. You can also
create a custom 404 page and set it up the No Route page settings. If you have a
Magento multistore, a custom 404 page is a must, because for each store Not Found
page contents will differ.
Create a new CMS page. When creating a new page, do not forget to specify Design ->
Layout. If there is a need to insert html code into the page content, disable the wysiwyg
editor. Empty Page Layout is used by default.
Afterward, switch to the needed store in the No Route Rage settings and select a new
CMS page.
Press Save Config button. As a result, you will get the following custom 404 page.
Magento also allows to create a custom noRoute handler. Let us make an example of
creating an alternative to 404 error page. This will be a noRoute page redirect to the
search page, where request path will serve as a search query.
To add a new “noRoute” handler, add to the etc/frontend/di.xml of your module the
following:
<type name="Magento\Framework\App\Router\NoRouteHandlerList">
<arguments>
<argument name="handlerClassesList" xsi:type="array">
<item name="custombvg" xsi:type="array">
<item name="class"
xsi:type="string">BelVG\CustomNoRoute\App\Router\NoRouteHandler</item
>
<item name="sortOrder" xsi:type="string">80</item>
</item>
</argument>
Afterward, create a custom noRoute handler and add the logic that would execute the
redirect.
1. \Magento\Framework\View\Layout::build() is called
2. \Magento\Framework\View\Layout\Builder::build() is called
3. \Magento\Framework\View\Model\Layout\Merge::load() is called
4. Handles from the var $handles to the protected field $handles are added.
5. Layout for the current handles is loaded from the cache. In case the layout is not
in the cache, then it is generated for each handle the following way, united and
stored in cache:
a. Layout for all handles of the current theme is loaded from cache. In case
there is no layout, it is generated the following way and then stored in
cache:
i. A physical theme, based on the current theme, is loaded. Physical
is the theme that has a designated folder and which is loaded via
registration.php. There are TYPE_PHYSICAL=0, TYPE_VIRTUAL=1,
TYPE_STAGING=2 types. Types are stored in t ype column in the
theme table at the database.
ii. The search of all *.xml files in the folders layout, page_layout in all
enabled modules and current themes (current themes = current
theme + all parent themes of it) is executed.
In order to view the merged layout for the current page, temporarily add execute method
into controller action before the return element the following code (at the condition that
result page is contained in the $resultPage variable):
This will put out the xml contents of the merged layout directly into the browser.
File <module_dir>/Observer/LayoutGenerateBlockObserver.php:
<?php
namespace Vendor\Module\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
Each type realizes the logic of headers and body generation for sending to the browser.
$output = $this->getLayout()->getOutput();
$this->assign('layoutContent', $output);
$output = $this->renderPage();
$this->translateInline->processResponseBody($output);
$response->appendBody($output);
} else {
parent::render($response);
}
return $this;
}
Layout instructions:
● <block> - for adding a new block
● <container> - for adding a new container
● before and after attributes - for specifying the position of a block or container.
You may use <block>, <container>, <move> in the blocks.
● <action> - [deprecated] calls block method during the block generation.
● <referenceBlock> and <referenceContainer> - for modifying or deleting the
current block and the container correspondingly.
● <move> - for altering the parent or the position of a block or a container.
<remove> - is used only to remove the static resources linked in a page <head>
section.
● <update> - is used to include a certain layout file.
● <argument> - sets arguments for blocks.
var_dump($resultPage->getLayout()->getUpdate()->getHandles());
die;
Add “.xml” to the received lines of code and this will be the layout files’ names
In case there is a need to modify the web store layout, you can create a new theme to
achieve this. To learn how to create a Magento theme, follow the link
https://belvg.com/blog/magento-2-creating-a-new-theme.html
To define theme hierarchy for your project, specify the parent theme in theme.xml file
inside the theme:
<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme
.xsd">
...
<parent>Magento/blank</parent>
...
</theme>
In order to customize a certain view file, override it in the current theme. Template
fallback process is when Magento 2 searches for a file in the child theme, then in the
parent themes and finally in modules. To learn more about this, follow the link:
https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/themes/theme-inherit.h
tml
● XDebug (The main peculiarity of this method are breakpoints as well as the
ability to view and modify variables at any time)
● Developer mode (the mode for development, displays errors directly on screen)
● Logs in folder var/logs and reports in folder var/reports
● Display Magento reports in browser
● Template path hints (displays templates’ paths and names)
To learn more about debugging methods, follow the link
(https://belvg.com/blog/developer-mode-and-debugging-in-magento-2.html)
First, log in to the admin panel and navigate to "Stores -> Configuration".
Then, go to "Advanced -> Developer". Expand the “Debug” tab and set "Enabled
Template Path Hints for Storefront" attributes at Yes.
(https://prnt.sc/k6d5cg)
In case you have multiple stores, set in the upper right menu the one for which you
enable path hints.
When there is a need to enable hints for a certain ip-address, expand the "Developer
Client Restrictions" tab and enter the IP-address into the "Allowed IPs (comma
separated)" field. If you need to enter several IP-addresses, separate them with
commas.
● bin/magento dev:template-hints:enable
● bin/magento dev:template-hints:disable
As you have completed the course of actions described above, path hints will appear at
the front end of the store.
(http://prntscr.com/k6d8su)
Following the highlighted paths, we can easily find the needed templates files.
Example:
If you need to override a file in the parent theme
<parent_theme_dir>/Magento_Catalog/templates/product/view/gallery.phtm
l, then create a file in the child theme
<child_theme_dir>/Magento_Catalog/templates/product/view/gallery.phtml
.
In order to override module’s view files, create them at the same path as they were
located in the module’s view folder, but deleting <area> from the path and adding the
module name. Example:
This method overrides templates files and all the files in web folder. However, if you try
to override the layout file the following way, instead of overriding the parent theme file
will be added up to. To learn more about the layout files override in Magento 2, follow
the link
(https://belvg.com/blog/override-a-layout-in-magento-2.html)
_beforeToHtml()
- data preparing
Magento\Catalog\Block\Product\ProductList\Related::_beforeToHtml(
)
- assign values
Magento\Backend\Block\Widget\Form\Element::_beforeToHtml()
- adding child's
Magento\Shipping\Block\Adminhtml\Create\Items::_beforeToHtml()
_toHtml()
Using this method, you can manipulate block rendering, complement the condition,
make wrappers for html, change the template for the block, etc.
Magento\GroupedProduct\Block\Order\Email\Items\Order\Grouped::_toHtml(
) , Magento\Sales\Block\Reorder\Sidebar::_toHtml()
To add static resources to the page, first create default_head_blocks.xml file at the
following path: <theme_dir>/Magento_Theme/layout/default_head_blocks.xml.
To add a locally located JavaScript file, use one of the following instructions:
<script src="Magento_Catalog::js/sample1.js"/>
<link src="js/sample.js"/>
In order to connect an external resource, add src_type attribute with “url” value. For
example:
<css
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-th
eme.min.css" src_type="url" />
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min
.js" src_type="url" />
To add a font, apply link tag and add attributes rel="stylesheet" and type="text/css":
<referenceContainer name="header.panel">
<block class="YouVendor\YouModule\Block\YouBlock"
name="new.block" />
</referenceContainer>
<block class="Magento\Catalog\Block\Product\View\Description"
name="product.info.sku" template="product/view/attribute.phtml"
after="product.info.type" />
To modify a block, apply referenceBlock instruction. Let us use the following block as
an example:
To modify its argument and add a new one, apply the following instruction:
<referenceBlock name="block.example">
<arguments>
<!-- Modified argument -->
In case you need to set a template for a block, there are two ways to do this.
<referenceBlock name="new.template"
template="Your_Module::new_template.phtml"/>
<referenceBlock name="new.template">
<arguments>
<argument name="template"
xsi:type="string">Your_Module::new_template.phtml</argument>
</arguments>
</referenceBlock>
It should be noted that the templates, set with template attribute, have a higher priority.
Therefore, the value in the template attribute will rewrite the one, specified with the help
of argument.
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/
page_configuration.xsd">
...
</page>
To pass the variables’ values from layout to block, apply <arguments> instruction inside
the block. For instance, if we want to pass cache_lifetime:
<referenceBlock name="my_block_name">
<page layout="2columns-left"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/
page_configuration.xsd">
...
</page>
Plain modules
Plain module is a common module that is not inherited from others. You will find an
example of a plain module above (Vendor_Module/js/script). In the examples below we
will use modules that are inherited with jQuery.widget, uiElement.extend functions. This
type of modules should be used when instances of the module are not connected to any
element.
jQuery UI widgets
jQuery UI widget allows to create a custom jQuery UI widget with a custom handler. This
type of module should be used when instances of the module are connected to certain
elements that are not UI components.
$.widget('mage.payment', {
options: {
...
},
_create: function () {
...
},
});
return $.mage.payment;
});
UiComponents
UiComponents allow to create a custom widget with a custom handler, inherited from
uiElement. This type of module is recommended to use when instances of the module
are connected to UI components.
define([
'underscore',
'uiElement',
'mageUtils',
'Magento_Catalog/js/product/storage/storage-service',
'Magento_Customer/js/section-config',
'jquery'
], function (_, Element, utils, storage, sectionConfig, $) {
...
return Element.extend({
defaults: {
...
},
initialize: function () {
...
return this;
},
});
});
Out-of-the-box UI components are heavily used in adminhtml, while at the frontend - only
in checkout (the UI components are added into checkout via layout). If JS module is a
component inside checkout or in form or grid in adminhtml, then apply UI components.
Otherwise, use regular JS module.
To insert a JS component into a PHTML template, use one of the following variants:
If you need to initialize JS module without the connection to HTML Element, use
<script type="text/x-magento-init">and enter " *"symbol into the selector field.
<script type="text/x-magento-init">
{
"*"
: {
"Vendor_Module/js/myfile": {"parameter":"value"}
}
}
</script>
In case you need to initialize JS module with connection to HTML Element, there are
two ways to do this:
<div id
="element-id"
data-mage-init='{"Vendor_Module/js/myfile":{"parameter":"value"}}'></
div>
Example:
vendor/magento/module-swatches/view/adminhtml/web/js/visual.js
...
$('[data-role=swatch-visual-options-container]').sortable({
distance: 8,
tolerance: 'pointer',
cancel: 'input, button',
axis: 'y',
....
}
});
...
Repository can be applied for working with collections in Magento 2. It realizes the
Repository pattern that allows to work with collections and entities regardless of their
storage place (storing or caching are the implementation details). The pattern itself is
located between Domain and Application Service Layer.
In Magento 2, five basic repository functions are realized. They are save, getById,
getList, delete, deleteById. Yet, each Repository realization in Magento has a custom
interface and the functions are not always implemented in it. For example, in
Magento\Quote\Api\GuestCartTotalRepositoryInterface, only get($cartId) method is
realized. Therefore, it is recommended to pay attention to a certain Repository class
implementation. Let us examine the Repository case using the
\Magento\Catalog\Api\ProductRepositoryInterface example and its realization
\Magento\Catalog\Model\ProductRepository.
<?php
namespace Magento\Catalog\A pi;
interface ProductRepositoryInterface
{
public function save(\Magento\Catalog\Api\Data\ProductInterface $product,
$saveOptions = false);
public function get($sku, $editMode = false, $storeId = null, $forceReload =
false);
public function getById($productId, $editMode = false, $storeId = null,
$forceReload = false);
public function delete(\Magento\Catalog\Api\Data\ProductInterface $product);
Here, all necessary five functions for working with Repository are realized. In this case,
the main class for working with product is
\Magento\Catalog\Api\Data\ProductInterface, allowing to save all the products
inherited from this class, regardless of their type.
<?php
...
protected $productRepository;
...
public
function someMethod() {
$product = $this->productRepository->getById(1);
$product->setSku('test-sku-1');
$this->productRepository->save($product);
}
...
...
public
function someDeleteMethod() {
$product = $this->productRepository->getById(1);
$this->productRepository->delete($product);
}
...
We modifies SKU of the project and saved it using save repository method. Delete
method deletes the product.
Unlike Api, Api/Data directory contains interfaces for the data, for example, store data or
customer data.
An excellent example for explaining the difference between Api and Api/Data is the
implementation of \Magento\Catalog\Api\ProductRepositoryInterface and
\Magento\Catalog\Api\Data\ProductInterface.
ProductRepository implements a get method that loads a Product object (using
ProductFactory) that implements ProductInterface for working with data.
In Magento 2, entities are unique objects that contain a number of various attributes
and/or parameters. Products, orders, users, etc. are all examples of entities.
...
$eavSetup>installEntities([
\Belvg\
Test\Model\Test::ENTITY => [
'entity_model' =>
Belvg\Test\Model\ResourceModel\Test',
'table' => \Belvg\Test\Model\Test::ENTITY .
'_entity',
'attributes' => [
'test_id' => [
'type' => 'static',
...
public
function install(SchemaSetupInterface $setup,
ModuleContextInterface $context)
{
$setup->startSetup();
$table = $setup->getConnection()->newTable(
$setup->getTable('belvg_test')
)->addColumn(
'entity_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'unsigned' => true, 'nullable' =>
false,
'primary' => true],
'EntityID'
)->addColumn(
'first_attribute',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
64,
This part of the code creates a belvg_test table with entity_id and first_attribute fields.
if (is_array($row)) {
$object->addData($row);
$this->loadAttributesForObject($attributes, $object);
this->_loadModelAttributes($object);
$
$this->_afterLoad($object);
$object->afterLoad();
$object->setOrigData();
$object->setHasDataChanges(false);
} else {
$object->isObjectNew(true);
...
public
function save(\Magento\Framework\Model\AbstractModel $object)
{
/**
* Direct deleted items to delete method
*/
if ($object->isDeleted()) {
return $this->delete($object);
}
if (!$object->hasDataChanges()) {
return $this;
}
$this->beginTransaction();
try {
$object->validateBeforeSave();
$object->beforeSave();
if ($object->isSaveAllowed()) {
if (!$this->isPartialSave()) {
$this->loadAllAttributes($object);
}
f ($this->getEntityTable() ==
i
\Magento\Eav\Model\Entity::DEFAULT_ENTITY_TABLE
&& !$object->getEntityTypeId()
) {
$object->setEntityTypeId($this->getTypeId());
}
$object->setParentId((int)$object->getParentId());
$this->objectRelationProcessor->validateDataIntegrity($this->getEntit
this->_beforeSave($object);
$
$this->processSave($object);
$this->_afterSave($object);
$object->afterSave();
}
$this->addCommitCallback([$object,
'afterCommitCallback'])->commit();
$object->setHasDataChanges(false);
} catch (DuplicateException $e) {
$this->rollBack();
$object->setHasDataChanges(true);
throw new AlreadyExistsException(__('Unique constraint
violation found'), $e);
} catch (\Exception $e) {
$this->rollBack();
$object->setHasDataChanges(true);
throw $e;
}
return $this;
}
...
To create additional fields in the database, apply Setup scripts. Let us examine how to
create a field in the database using InstallSchema class as an example:
...
public
function install(SchemaSetupInterface $setup,
ModuleContextInterface $context)
{
$installer = $setup;
$installer->startSetup();
$table = $installer->getTable('custom_table');
$columns = [
'custom_field' => [
'type' => \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
'nullable' => false,
'comment' => 'custom_field',
],
];
$connection = $installer->getConnection();
foreach ($columns as $name => $definition) {
$connection->addColumn($table, $name, $definition);
}
$installer->endSetup();
}
...
public
function __construct(EavSetupFactory $eavSetupFactory)
{
Code for extension attribute is generated during the compilation process, using
\Magento\Framework\Code\Generator that applies etc/extension_attributes.xml file
from the module directory. Example of extension_attributes.xml :
<extension_attributes for="Magento\Sales\Api\Data\OrderInterface">
<attribute code="custom_extension_attribute"
type="Belvg\Extension\Api\Data\CustomExtensionAttributeInterface" />
</extension_attributes>
Unlike the common Magento attributes, extension attribute is not automatically loaded
from and stored into the database, which means you need to realize the loading and
saving manually. For this purpose, plugins are the best choice; they are declared in
di.xml file. Example:
<type name="Magento\Sales\Api\OrderRepositoryInterface">
<plugin name="custom_extension_attribute"
type="Belvg\Extension\Plugin\OrderPlugin"/>
</type>
In the plugin, we can realize afterGet and afterSave methods that will contain the
loading and saving extension attribute logic.
For example:
$productCollection->addFieldToSelect("custom_field");
Example:
$productCollection->addFieldToFilter('entity_id', array('in' =>
[1,2,3])
setOrder method is used for sorting and processes both filter and
direction fields. For instance:
$productCollection>setOrder('position','ASC');
Database layer can have put our exceptions depending on its realization. For example,
PDO/Mysql can put out the following exceptions:
Zend_Db_Adapter_Exception
Zend_Db_Statement_Exception
Zend_Db_Exception
Zend_Db_Statement_Pdo
PDOException
LocalizedException
InvalidArgumentException
Exception
DuplicateException
If Magento detects a new module, then it will instantiate objects from the
Vendor\Module\Setup\InstallSchema and Vendor\Module\Setup\InstallData classes.
In case the module version has changed, then Vendor\Module\Setup\UpgradeSchema
and Vendor\Module\Setup\UpgradeData will be instantiated. Afterward, the
corresponding upgrade methods will be executed.
Versioning
Unlike Magento 1, Magento 2 does not contain the inbuilt migration versioning tools,
meaning that a developer must check the current module version manually.
$setup->endSetup();
}
}
InstallSchema class
This setup script is used for modifying database structure at the module’s first
installation.
<?php
namespace Vendor\Module\Setup;
$table = $setup->getConnection()->newTable(
$setup->getTable('custom_table' )
)->addColumn(
'custom_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'unsigned' => ,
true 'nullable' => ,
false 'primary'
=> true],
'Custom Id'
)->addColumn(
'name',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
255,
[],
'Custom Name'
)->setComment(
'Custom Table'
);
$setup->getConnection()->createTable($table);
$setup->endSetup();
}
InstallData class
This setup script is applied for adding and modifying the data at the module’s first
installation.
<?php
namespace Vendor\Module\Setup;
$setup->endSetup();
}
}
UpgradeSchema class
The setup script is applied for modifying database structure at the module update.
<?php
namespace Vendor\Module\Setup;
UpgradeData class
<?php
namespace Vendor\Module\Setup;
Recurring scripts
Recurring scripts are run each time setup:upgrade command is launched and depend
on the module’s version.
<module_dir>/Setup/Recurring.php
<?php
namespace Vendor\Module\Setup;
In the Flat model, attribute values are stored in the same table as the entities; a separate
column is created for each attribute in the table.
In the EAV model, attribute values are stored in a separate table. A separate column is
not created for each attribute, and a new row is created for each attribute value of an
entity in the EAV table.
Magento receives entity attribute values as one large SQL query, which is generated by
the following algorithm:
1. Get all attribute tables for a specific entity_type
2. For each table, do the following:
● a select subquery is created from the current table, which requests value and
attribute_id
● the condition is added that entity_id = ID of the requested entity *
● a condition is added for each scope that store_id IN ($ scope-> getValue ()) **
● sorted by store_id in a descending order
Text Field
Let’s examine how to create a new attribute of the Text Field type.
Dropdown
210 155 1
211 155 2
212 155 3
208 211 0 2
210 212 0 3
206 210 0 1
Price
Differences compared to Text Field:
● eav_attribute.frontend_input = “price”
● eav_attribute.backend_model =
“Magento\Catalog\Model\Product\Attribute\Backend\Price”
● eav_attribute.backend_type = “decimal”
Media image
Differences compared to Text Field:
● eav_attribute.frontend_input = “media_image”
1 213 0 0 1
6 215 1 0 3
2 213 1 0 1
4 214 1 0 2
3 214 0 0 2
5 215 0 0 3
● frontend_model - a class that describes the field display in the frontend section
of a site. Inherited from Magento \ Eav \ Model \ Entity \ Attribute \ Frontend \
Text Field
Text Area
Has the form of a many-line input field. A WYSIWYG editor can be enabled for it in the
attribute settings.
Yes/No
Has the form of a switcher with two entities: Yes or No.
Multiple Select
Has the form of a list that offers to select several values, or no value at all.
Dropdown
Has the form of a drop-down list that offers to select only one value.
Media Image
Media Image is different from any other fields, for it has no field. Instead, an attribute of
Media Image type adds role to the product image and video.
Text Swatch
Is provided similarly to Visual Swatch, only the button content is in text format.
Below is a screenshot, where Design and Schedule Design Update are attribute groups,
and the nested elements are attributes.
Attributes in attribute set settings:
Before saving an EAV entity at the client side, we have the following features:
Before saving an EAV entity at the server side, we have the following features:
● Check attribute fields with is_required = 1 for fullness. Magento checks the
required fields on both the client and server sides.
● Uniqueness check of the attribute fields with is_unique = 1.
● Perform operations in backend_model
○ Validation (validate method). Allows to realize additional server check
before saving.
○ Operation before saving (beforeSave method)
○ Operation after saving ( afterSave method)
Module catalog has an additional catalog_eav_attribute table for attributes, where the
following parameters are stored:
● backend_model
● source_model
● attribute_model
● frontend_model
Attribute Model
Backend Model
The model is used for processing and validating the attribute values.
return true;
}
}
Source Model
The model is used for providing the list of the attribute values.
Example:
class TestSource extends
\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource
{
public function getAllOptions()
{
if (!$this->_options) {
Frontend Model
Example:
class TestFrontend extends
\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend
{
public function getValue(\Magento\Framework\DataObject $object)
{
$attribute_code = $this->getAttribute()->getAttributeCode();
$value = $object->getData($attribute_code);
return nl2br(htmlspecialchars($value));
}
}
For a developer, there is no big difference between EAV and Flat. Models, resource
models and collections are created similarly.
Collections
EAV
<?php
namespace Belvg\Geo\Model\ResourceModel\City;
FLAT
<?php
namespace Belvg\Geo\Model\ResourceModel\Country;
Resource model
EAV
<?php
namespace Belvg\Geo\Model\ResourceModel;
FLAT
<?php
namespace Belvg\Geo\Model\ResourceModel;
Models
EAV
<?php
namespace Belvg\Geo\Model;
FLAT
<?php
namespace Belvg\Geo\Model;
Additionally, the following methods, that contain the attribute as its code or its object,
are added into EAV collections:
● addAttributeToSelect converts attribute into its code and calls addFieldToSelect
Additionally, in EAV resource models, the methods for working with attributes are added
and methods load, save, delete are overridden.
The key difference between EAV and Flat lies in data storage.
The configuration settings in the admin panel have Use Flat Catalog Category and Use
Flat Catalog Product options. The settings allow to edit the sources of the uploaded
products and categories, changing them from EAV tables for FLAT index tables.
Product attribute gets to the flat table in case it complies to at least one condition:
if ($this->isEnabledFlat()) {
...
} else {
...
}
Therefore, if Flat is enabled, one action is performed, but in case it is disabled, then
another action is triggered.
$this->_init(\Magento\Catalog\Model\ResourceModel\Category::class);
}
}
Using EAV for a new entity is advisable in case at least one of the following conditions
is true:
1. There is a scope.
2. Admins and modules have the ability to add attributes into an entity or modify
attributes backend type.
3. Potentially, the number of columns in Flat model can exceed 1017.
4. Potentially, the required amount of indexes, like INDEX in Flat model, can exceed
64 or reach the amount when the entities’ adding / modifying / deletion will be
slow.
2. Quickly add a new attribute. Adding a new attribute does not change the EAV
table in any way. In Flat, you need to add a new column. The ALTER TABLE
operation is also slow. This is especially noticeable in large tables. *
3. Change the backend type attribute faster. To change the attribute type in EAV,
you need to move the this attribute data from one table to another. For Flat, you
need to perform ALTER TABLE. *
<type name="Magento\Framework\EntityManager\HydratorPool"
>
<arguments>
<argument name="hydrators" =
xsi:type "array"
>
By default, CheckIfExists operation checks for the existence of an entry in the main
table with a direct SQL query.
● Magento\Eav\Model\ResourceModel\CreateHandler
● Magento\Eav\Model\ResourceModel\UpdateHandler
● Magento\Eav\Model\ResourceModel\ReadHandler
● Changes the value, if the attribute is modified relative to Snapshot and the new
value is not empty or the attribute allows empty values
● Deletes, if the attribute is changed relative to Snapshot and the new value is
empty and the attribute does not allow empty values
● Creates, if the attribute is absent in Snapshot and the new value is not empty or
the attribute allows empty values
A. a select subquery is created from the current table, which requests value and
attribute_id
B. the condition is added that entity_id = ID of the requested entity
C. a condition is added for each scope from the context that store_id IN ($scope->
getValue ())
D. sorted by store_id in descending order
In Magento, scope in EAV is realized due to store_id column in the EAV attribute value
tables.
● store_id = 0, if scope global
● store_id = IDof the selected Store View, if not global
It turns out that number of websites / stores does not directly affect * the upload speed,
because when StoreScopeProvider is used during loading, the context contains no more
than two store_id, i.e. does not depend on the number of websites / stores.
* the number of websites / stores always indirectly affects the loading of attribute
values, as the more rows in the attribute value tables, the slower is the load.
Let us consider the value of store_id while maintaining the attribute value.
Thus, the attribute saving speed, depending on the number of websites / stores, is
affected by the presence of modified attributes from the scope website. The more Store
Views are in the Website, the more entries in the database you need to create or modify
entries, the slower is the saving.
You can partially override, for example, a single read operation, then the default
operations will be used for the rest of the operations.
We can also override work operations with the attributes for EAV:
Extensions override:
Example:
<?php
namespace Vendor\Module\Setup;
use Magento\Eav\Setup\E
avSetupFactory ;
use Magento\Framework\S etup\InstallDataInterface
;
use Magento\Framework\S etup\ModuleContextInterface
;
use Magento\Framework\S etup\ModuleDataSetupInterface
;
$eavSetup->addAttribute(
\Magento\Catalog\Model\Product::ENTITY,
'my_attribute',
[
'type' => 'int',
'label' => 'My Attribute',
'input' => 'select' ,
'source' =>
\Magento\Eav\Model\Entity\Attribute\Source\Table::class,
'required' => false,
'option' => ['values' => [ ,
'Value 1' ,
'Value 2' 'Value 3'
]]
]
);
}
}
required is_required 1
user_defined is_user_defined 0
unique is_unique 0
visible is_visible 1
searchable is_searchable 0
filterable is_filterable 0
comparable is_comparable 0
visible_on_front is_visible_on_front 0
wysiwyg_enabled is_wysiwyg_enabled 0
is_html_allowed_on_front is_html_allowed_on_front 0
visible_in_advanced_searc is_visible_in_advanced_sea 0
h rch
filterable_in_search is_filterable_in_search 0
used_in_product_listing used_in_product_listing 0
used_for_sort_by used_for_sort_by 0
position position 0
used_for_promo_rules is_used_for_promo_rules 0
is_used_in_grid is_used_in_grid 0
is_visible_in_grid is_visible_in_grid 0
is_filterable_in_grid is_filterable_in_grid 0
The purpose of the interface is to decrease the code dependency (dependency inversion
principle).
return true;
}
}
To log in to the admin panel, you need to know its basic URL (for example,
https://www.website.com/admin_1pf3534g/) and then pass authentication.
Controller actions and blocks in adminhtml are located in the Adminhtml subfolder of
Controller and Block folders respectively.
Controller actions and blocks in adminhtml inherit the classes, different from those
inherited by actions and blocks in frontend.
In adminhtml:
Block inherits Magento\Backend\Block\Template
Action inherits Magento\Backend\App\AbstractAction
In frontend:
Block inherits Magento\Framework\View\Element\Template
Action inherits Magento\Framework\App\Action\Action
UI Components
ui component file name matches the ui component name (except for the .xml suffix),
that can be added into layout using the following instructions:
<uiComponent name="notification_area"
aclResource="Magento_AdminNotification::show_list"/>
Ui_component folder contains one or several XML declarations for grids or forms.
On the screenshot, you can see .xml file for template definition. It contains a link to the
listing basic component connection, aimed at displaying grids, lists and fragments.
Sorting, search and dividing into pages is added to the built-in functions, which are very
helpful for new components and modules development.
● Filters
● Pagination
These are secondary components that serve as additional elements for expanding the
basic components. Using them is not obligatory, but it is a good practice for a
developer, for they will:
● Create a unified and clear interface
● Simplify and speed up the development process
● Create a simple and intellectually clear environment for support and expansion.
To connect Ui components to your module, located in the admin part of the interface,
Magento uses a specialized xml file:
<module_dir>/view/adminhtml/ui_component/<ui_component_name>.xml.
Columns Control is responsible for displaying and hiding active list columns.
Filters component receives the information about the required secondary elements; in
this case, it is an element of select form.
Using <argument name=”class”> attribute, we rewrite the standard element class into
the one we need.
All components contain information about the current state and receive users’ values.
For example, Columns Control receives the parameters on the minimal or maximal
number of the displayed columns.
As you can see, UI components library is the main instrument for the Mangento admin
area, and a considerable part of the store's admin panel is built with it:
Form
Contains modified fields of a certain entity. To create a form, you need to:
Grid
Contains the list of entities, filters, bookmarks, paging, columns controls. Allows to
search entity, modify list sorting, hide and add columns, change their order. To create a
grid, you need to:
1. Create listing xml file of UI component configuration:
<module_dir>/view/<area>/ui_component/<ui_component_name>.xml
2. Set DataSource
3. Add filter, columns, toolbar, ...
4. Add ui component into layout: <uiComponent name="<ui_component_name>"/>
For the form component, the data of an entity are passed inside the current page’s html
code in the <script type = "text / x-magento-init"> ... </script> tag.
For the grid component, the data is loaded through an additional ajax request for the
controller action m
ui / index / render, processed by the \ Magento \ Ui \ Controller \
Adminhtml \ Index \ Render class for adminhtml area and \ Magento \ Ui \ Controller \
Index \ Render for frontend area .
Let us consider the examine of a form. It includes text, textarea, select, multiselect,
image. The data is loaded via the Vendor\Module\Model\MyEntity\DataProvider class.
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.x
sd">
<field name="name">
<argument name="data" xsi:type= "array">
<item n ame="config" xsi:type= "array">
<item n ame="dataType" xsi:type="string"
>t ext
</item>
<item n ame="label" translate=
"true"
xsi:type=
"string">Name</item>
<item n ame="formElement" xsi:type="string" >input</ item>
<item n ame="source" xsi:type=
"string">MyEntity</ item >
<item n ame="sortOrder" xsi:type="number">50</ item
>
<item n ame="validation" xsi:type="array">
<item n ame
="required-entry" xsi:type= "boolean" >
true</
item
>
</item>
</item>
</argument>
</field>
<field name="about">
<argument name="data" xsi:type= "array">
<item n ame="config" xsi:type= "array">
<item n ame="dataType" xsi:type="string"
>t ext
</item>
<item n ame="label" translate=
"true"
xsi:type=
"string">About</item>
<item n ame="formElement" xsi:type="string" >textarea</item
>
<item n ame="source" xsi:type=
"string">MyEntity</ item>
<item n ame="sortOrder" xsi:type="number">65</ item
>
</item>
</argument>
</field>
<field name="image">
<argument name="data" xsi:type= "array">
<item n ame="config" xsi:type= "array">
<item n ame="label" xsi:type=
"string">Image</item
>
<item n ame="visible" xsi:type= >
"boolean" true
</
item
>
<item n ame="source" xsi:type="string"
>MyEntity</item
>
<item n ame="formElement" xsi:type="string">fileUploader</item
>
<item n ame="elementTmpl"
xsi:type=
"string">ui/form/element/uploader/uploader</ item
>
<item n ame="previewTmpl"
Result:
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.x
sd">
<argument name="context" xsi:type=
"configurableObject"
>
<argument name="class"
xsi:type="string">Magento\Framework\View\Element\UiComponent\Context</argument>
<argument name="namespace" xsi:type=
"string"
>my_listing_index</argument>
</argument>
Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider
</argument>
<argument name="name"
xsi:type=
"string">my_listing_grid_data_source</argument>
<argument name="primaryFieldName" xsi:type= "string">id
</argument>
<argument name="requestFieldName" xsi:type= "string">id
</argument>
<argument name="data" xsi:type= "array">
<item n ame="config" xsi:type= "array" >
<item n ame="component"
xsi:type=
"string">Magento_Ui/js/grid/provider</ item
>
<item n ame="update_url" path="mui/index/render"
xsi:type=
"url"/>
<item n ame="storageConfig" xsi:type= "array"
>
<item n ame
="indexField" xsi:type= "string" d
>i </
item
>
</item>
</item>
</argument>
</argument>
</dataSource>
<listingToolbar name="listing_top" >
<argument name="data" xsi:type= "array"
>
<item name="config" xsi:type="array" >
<item n ame="sticky" xsi:type= "boolean" >
true</
item>
</item>
Result:
<?xml version="1.0"?>
<source_model>Vendor\Module\Model\Config\Source\Custom </source_model>
</field>
<field id=" custom_text"
translate =
"label" =
type "text"
="20" showInDefault="1"
sortOrder showInWebsite=
"1" showInStore="1"
>
<label>Custom Text</label>
</field>
<field id="
logo" =
translate "label" =
type "image" =
sortOrder "30"
showInDefault="1" showInWebsite= "1"
showInStore="1">
<label>Custom Image</label>
<backend_model>Magento\Config\Model\Config\Backend\Image </backend_model>
<upload_dir config =
"system/filesystem/media"
scope_info="1">logo</upload_dir>
<base_url type ="media"
scope_info =
"1" >
logo
</base_url>
<comment><![CDATA[Allowed file types: jpeg, gif,
png.]]></comment>
</field>
<field id=" depends_example" translate=
"label" type="text"
sortOrder="40" showInDefault="1" showInWebsite =
"1"
showInStore ="1"
>
<label>Dependant text field example with validation </label>
<depends>
<field id="
*/*/custom_dropdown" >
1</field>
</depends>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted
</backend_model>
</field>
</group>
</section>
</system>
</config>
Then
This code adds a new section and assigns it to custom_tab tab. Here, we can use any
tab from the existing ones.
showInDefault="1", showInWebsite="1" and showInStore="1" parameters set the scope
where our section will be displayed.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/c
onfig.xsd">
<default>
<custom_section>
<general>
<yesno_dropdown>1</enable>
<custom_text>Test Value</display_text>
</general>
</helloworld>
</default>
</config>
Yes/No Dropdown
<field id="yesno_dropdown" translate="label" type="select"
sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Custom Yes/No Dropdown</label>
<source_model>Vendor\Module\Model\Config\Source\Custom</source_model>
</field>
<?php
namespace Vendor\Model\Model\Config\Source;
class Custom implements \Magento\Framework\Option\ArrayInterface
{
/**
*
@return array
*/
public function toOptionArray()
{
return [
['value' => 0, 'label' => __('Zero')],
['value' => 1, 'label' => __('One')],
['value' => 2, 'label' => __('Two')],
];
}
}
Dependent Field
Textarea
<field id="custom_textarea" translate="label" type="textarea"
sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Custom Textarea</label>
Secret Field
<field id="custom_secret" type="obscure" translate="label"
sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Custom Secret Field</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend
_model>
</field>
type=”obscure” hides field’s value from the frontend, but the information from it will still
be saved as plain text. Setting Magento\Config\Model\Config\Backend\Encrypted as a
backend model allows to encrypt the data in the database.
Most of the source models are located in
app/code/Magento/Config/Model/Config/Source and backend models are located in
app/code/Magento/Config/Model/Config/Backend.
<module_dir>/Helper/Config.php
<?php
namespace BelVG\Test\Helper;
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Store\Model\ScopeInterface;
/**
*
@return string
*/
public function getTestValue()
{
return (string)
$this->scopeConfig->getValue(self::XML_PATH_TEST_VALUE,
ScopeInterface::SCOPE_STORE);
}
}
If your configuration values are encrypted and stored in the database, set
backend_model in etc/config.xml file so that scopeConfig->getValue would return the
decrypted configuration values. Example:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/c
onfig.xsd">
<default>
<belvg>
<settings>
<test_value
backend_model="Magento\Config\Model\Config\Backend\Encrypted"/>
</settings>
</belvg>
</default>
</config>
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc
/menu.xsd">
<menu>
<add id="Vendor_Module::second_level" title="Second Level
Menu" module="Vendor_Module" sortOrder="10"
action= "Vendor_Module/action_path"
resource="Magento_Backend::content"
parent= "Magento_Backend::system_design
l"/>
</menu>
</config>
where
<module_dir>/etc/adminhtml/menu.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc
/menu.xsd">
<menu>
<add id="Vendor_Module::first_level" title="First Level Menu"
module= "Vendor_Module" sortOrder="51"
resource="Magento_Backend::content"/>
<add id="Vendor_Module::second_level" title="Second Level
Menu" module="Vendor_Module" sortOrder="10"
action= "Vendor_Module/action_path"
resource="Magento_Backend::content"
parent= "Vendor_Module::first_level"/>
</menu>
</config>
<module_dir>/etc/adminhtml/menu.xml
<?xml version="1.0"?>
Defining ACL
Use <module_dir>/etc/acl.xml file to create a new ACL rule.
<?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="Magento_Backend::admin">
<resource id="Vendor_Module::acl1" title="ACL Parent
Rule" sortOrder="51">
where,
● id - resource identifier. Applied in menu and actions configuration for identifying
the resource. It should be unique and set in the following format:
Vendor_Module::resource_name.
● title - resource title
● sortOrder - resource order in resource tree
System section > Permissions > User Roles contains all the available roles and their
permissions.
System section > Permissions > All Users allows to create new users or modify the
current users, as well as assign user roles.
$adminUser = $this->userFactory->create();
$adminUser->setRoleId(ROLE_ID)
->setEmail('admin' . $i . '@example.com')
->setFirstName('Firstname')
->setLastName('Lastname')
->setUserName('admin' . $i)
->setPassword('123123q')
->setIsActive(1)
->save();
In the out-of-the-box Magento 2 Community Edition, there are six product types:
1. Simple Product. This is a basic and the most popular product type. A single
simple product corresponds to a single physically existing product to a unique
SKU (Store Keeping Unit).
Simple Product
2. Virtual Product. This product type is for the products that do not exist physically.
Paid subscriptions, services, insurances, etc. are examples of Virtual products.
3. Configurable Product. The type allows to create products with a list of options,
for example, for example, a T-shirt in different colors and sizes. Each product
option of such configurable product corresponds to a single Simple Product with
a unique SKU, allowing the retailer to keep track of each product option stock
balance.
4. Grouped Product. This product type allows to group single Simple or Virtual
Products into bundles. Therefore, the customer can buy all the needed items at
once, without adding them to the cart separately. Moreover, the customer gets to
decide which products from the bundle he wants to purchase and in what
amount. The products he or she selected are added to the cart separately.
5. Bundle Product. This product type allows customers to create a bundle at their
wish, using a set of options (for example, a yoga kit).
Bundle Product
SKU and Weight attributes can be fixed or dynamic. The price for Bundle Product can be
set as Price Range (from minimum to maximum) or As Low As (the lowest price
possible). The admin can set how the products will be delivered - together or separately.
6. Downloadable Product. This product type is aimed at digital products that can
consist of one or several files, downloaded by the customer after the purchase is
made.
Virtual and Downloadable Products weight nothing, which means there is no need to
deliver them and no need to select a delivery option at the checkout. Also, Virtual and
Downloadable Products do not have In Stock attribute.
Configurable:
- getConfigurableAttributes- gets the attributes, used for subproducts;
- getUsedProductIds- gets id subproducts;
- getProductByAttributes- gets products by their attribute values;
- getConfigurableOptions- gets options list;
- setImageFromChildProduct- sets the image of a child product for a parent
product, if it was not set previously.
Bundle:
- getOptions- gets the list of options;
- getSelectionsCollection- gets the selections collection by their id;
- getSpecifyOptionMessage- gets the customer message with the request to
specify options;
- checkIsAllRequiredOptions- checks whether all the required options are
selected;
- checkSelectionsIsSale- checks if all the options are available for sale.
Downloadable:
As a rule, each product in Magento 2 can have several prices: regular, special, final, etc.
Each price type has a class, accountable for calculating the final value of a certain price
type. Some price types are available for all the products, while others are specific to a
certain product type.
Let us examine the basic price types, available for all product types:
- downloadable products:
- link_price- price when the product is downloaded from the link
provided;
- bundle products:
- bundle_option- price of bundle product option.
Each price type has getValue() and getAmount() methods. getValue() method returns
the price value, while getAmount() method returns the final price with all the taxes
added.
For simple and virtual product types, final_price corresponds to the minimal
regular_price, catalog_rule_price, special_price, tier_price values.
For configurable products, the price of each option is selected as a minimal value from
base_price, tier_price, index_price, catalog_rule_price.
At the same time, when choosing the minimum price, the status of the configurable
product option, its availability for a particular site and availability in stock are checked.
After determining the final_price of all options, the final price of the configurable
product is determined, which will be equal to the lowest cost of its options.
final_price of grouped product equals the minimal final_prices of all the products from
the group.
1. Create a new price type and add it to the basic price set. For this, create a
custom module with the class that will realize
Magento\Framework\Pricing\Price\BasePriceProviderInterface interface
Magento\Framework\Pricing\Price\BasePriceProviderInterface and expand
Magento\Framework\Pricing\Price\AbstractPrice class; also, add the necessary
instructions in the module’s di-file:
<virtualType name="MyVendor\MyModule\Pricing\Price\Pool"
type="Magento\Framework\Pricing\Price\Pool">
<arguments>
<argument name="prices" xsi:type="array">
<item name="my_price"
xsi:type="string">MyVendor\MyModule\Pricing\Price\MyPrice</item
>
</argument>
<argument name="target"
xsi:type="object">Magento\Catalog\Pricing\Price\Pool</argument>
</arguments>
</virtualType>
2. Create a plugin for the class methods of that price type, the calculation process
of which you need to modify; for example, around getValue() method.
3. Override the class, corresponding to the required price type, for a certain product
type by using di-file in your module:
<virtualType name="MyVendor\MyModule\Pricing\Price\Pool"
type="Magento\Framework\Pricing\Price\Pool">
4. Completely override the price class using the preference instruction in di-file of
the module.
<block class="Magento\Catalog\Pricing\Render"
name="product.price.myprice">
<arguments>
Using data-arguments, you can modify the price box, amount renders and adjustment
renders, by modifying their сss-classes, id_suffix, id_prefix, etc.
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/
layout_generic.xsd">
<referenceBlock name="render.product.prices">
<arguments>
<argument name="myprice" xsi:type="array">
<item name="prices" xsi:type="array">
<item name="final_price" xsi:type="array">
<item name="render_class"
xsi:type="string">MyVendor\MyModule\Pricing\Render\FinalPriceBox</ite
m>
<item name="render_template"
xsi:type="string">MyVendor_MyModule::product/price/final_price.phtml<
/item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</layout>
Anchor - sets the display of the layered navigation in the given category.
The settings from Search Engine Optimization and Products in Category tabs are rather
intuitive. The first allows to add meta descriptions to the categories, while the second
allows to add the products, displayed in the given category.
Add Root Category button creates a new category in the categories tree root. All the
root categories are further used for the multistore settings.
Add Subcategory button creates a subcategory inside the active category.
To realize category tree in Magento, the following logic is used. The picture below
demonstrates the catalog_category_entity table structure, where the information about
categories structure is stored.
The construction of the category tree begins with an entry with the parent_id = 0 value.
This entry has a value of entity_id = 1. Categories whose parent_id = 1 act as Root
Categories. And then the construction of the category tree is implemented through the
connection parent_id <---> entity_id.
When creating a category, the only required value is Category name. Based on this
name, the URL Key is automatically generated, which is the path by which the category
will be displayed in the browser.
Another example:
We have 5 catalog rules, each applies to 4 customer groups, 5 websites and 2000
products. Then, the number of lines will be the following:
catalogrule_product: 5 * 4 * 5 * 2000 = 200000
catalogrule_product_price: 200000 * 3 = 600000.
Now, we will examine the reason why the influence on the page upload is relatively
insignificant. Let us turn to Magento code.
In vendor\magento\module-catalog-rule\etc\frontend\events.xml file, set the observer
for catalog_product_get_final_price event.
https://i.imgur.com/XuicwTk.png
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events
.xsd">
<event name="catalog_product_get_final_price">
<observer name="catalogrule"
instance="Magento\CatalogRule\Observer\ProcessFrontFinalPriceObserver
Then, in magento\module-catalog-rule\Observer\ProcessFrontFinalPriceObserver.php
file, we get the price and assign it to the product.
https://i.imgur.com/Tx3qb7L.png
public
function execute(\Magento\Framework\Event\Observer $observer)
{
$product = $observer->getEvent()->getProduct();
$pId = $product->getId();
$storeId = $product->getStoreId();
if ($observer->hasDate()) {
$date = new \DateTime($observer->getEvent()->getDate());
} else {
$date = $this->localeDate->scopeDate($storeId);
}
if ($observer->hasWebsiteId()) {
$wId = $observer->getEvent()->getWebsiteId();
} else {
$wId =
$this->storeManager->getStore($storeId)->getWebsiteId();
}
if ($observer->hasCustomerGroupId()) {
$gId = $observer->getEvent()->getCustomerGroupId();
} elseif ($product->hasCustomerGroupId()) {
$gId = $product->getCustomerGroupId();
} else {
https://i.imgur.com/3vfkW4u.png
return false;
}
public
function getRulePrices(\DateTimeInterface $date, $websiteId,
$customerGroupId, $productIds)
return $connection->fetchPairs($select);
}
In action
vendor\magento\module-catalog-rule\Controller\Adminhtml\Promo\Catalog\ApplyRule
s.php, a copy of
\Magento\CatalogRule\Model\Rule\Job class is created, in which applyAll() method is
called. https://i.imgur.com/v7FfJK5.png .
if ($ruleJob->hasSuccess()) {
$this->messageManager->addSuccess($ruleJob->getSuccess());
$this->_objectManager->create(\Magento\CatalogRule\Model\Flag::class)
->loadSelf()->setState(0)->save();
} elseif ($ruleJob->hasError()) {
$this->messageManager->addError($errorMessage . ' ' .
$ruleJob->getError());
}
} catch (\Exception $e) {
$this->_objectManager->create(\Psr\Log\LoggerInterface::class)->criti
cal($e);
$this->messageManager->addError($errorMessage);
}
**
/ @var \Magento\Backend\Model\View\Result\Redirect
$resultRedirect */
$resultRedirect =
$this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
return $resultRedirect->setPath('catalog_rule/*');
}
After all these actions and if the price rule is configured correctly (if date range and
conditions are both correct), the rules will work.
In case the rules do not work, navigate to catalogrule_product_price table and check the
product price there. If the data is incorrect, try checking the logs, disconnecting
third-party modules or review the Catalog Price Rule reindex process with xDebug.
Rules in checkout
With Cart Price Rules, we can modify the final price at the checkout and the shipping
costs. It is also possible to configure rules via the admin panel. There is a flexible set of
conditions, under which the rules apply. We can add a coupon for applying the discount
and indicate its percentage. Also, we can apply a discount or include free shipping to
the cart that meets certain conditions. We can see all the settings in the Marketing->
Cart Price Rules-> Add New Rule section. As conditions, we can apply:
The “Magento \ SalesRule \ Model \ Rule” model is responsible for the cart rules, and
with it we can programmatically add or get a specific cart rule.
The more there are cart rules without coupons for the current website and for the
current customer group, the slower is the processing of cart rules. It is not
recommended to have a lot of such cart rules.
<extension_attributes for="Magento\Quote\Api\Data\CartInterface">
<attribute code="shipping_assignments"
type="Magento\Quote\Api\Data\ShippingAssignmentInterface[]" />
</extension_attributes>
Use “Quote Repository Plugin” to fill in the values with the afterLoad (), beforeSave (), or
whenever () functions. Quote does not use “custom_attributes” since they are not EAVs.
Inventory validation
● Use Magento\Quote\Model\Quote\Item setQty() function to declare quote item
quantity
● Use event "sales_quote_item_qty_set_after"
● Use function
\Magento\CatalogInventory\Model\Quote\Item\QuantityValidator::validate to
check product inventory data when quote item quantity declaring.
Add to cart
● Use function Magento\Quote\Model\Quote addProduct() to add a product to the
shopping cart Returns error message if product type instance can't prepare
product.
● Use function Magento\Catalog\Model\Product\Type\AbstractType
prepareForCartAdvanced() to initialize product(s) to add to cart process. The
advanced version of function that prepares product for cart (processMode) can
be specified there.
● Use function Magento\Quote\Model\Quote\Item\Processor::prepare to set
quantity and custom price for quote item
● Use event "sales_quote_product_add_after"
● Use event "sales_quote_save_after", "sales_quote_save_before"
● Use event "checkout_cart_add_product_complete"
First, we need to create the <module_dir>/etc/sales.xml file in our module. This file is
applied for registration of all the available Magento totals.
<module_dir>/etc/sales.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/s
ales.xsd">
<section name="quote">
<group name="totals">
<item name="custom_total"
instance="Vendor\Module\Model\Totals\Custom" sort_order="500"/>
</group>
</section>
</config>
<module_dir>/Model/Total/Custom.php
<?php
Namespace Vendor\Module\Model\Total;
/**
*
@param \Magento\Quote\Model\Quote $quote
*
@param \Magento\Quote\Api\Data\ShippingAssignmentInterface
$shippingAssignment
*
@param \Magento\Quote\Model\Quote\Address\Total $total
*
@return $this
*/
public function collect(
\Magento\Quote\Model\Quote $quote,
$items = $shippingAssignment->getItems();
if (!count($items)) {
return $this;
}
$total->setTotalAmount('custom_total', $amount);
$total->setBaseTotalAmount('custom_total', $amount);
$total->setCustomAmount($amount);
$total->setBaseCustomAmount($amount);
$total->setGrandTotal($total->getGrandTotal() + $amount);
$total->setBaseGrandTotal($total->getBaseGrandTotal() +
$amount);
return $this;
}
/**
*
@param \Magento\Quote\Model\Quote\Address\Total $total
*/
protected function
clearValues(\Magento\Quote\Model\Quote\Address\Total $total)
{
$total->setTotalAmount('subtotal', 0);
$total->setBaseTotalAmount('subtotal', 0);
$total->setTotalAmount('tax', 0);
$total->setBaseTotalAmount('tax', 0);
$total->setTotalAmount('discount_tax_compensation', 0);
$total->setBaseTotalAmount('shipping_discount_tax_compensation', 0);
$total->setSubtotalInclTax(0);
$total->setBaseSubtotalInclTax(0);
}
/**
*
@param \Magento\Quote\Model\Quote $quote
*
@param \Magento\Quote\Model\Quote\Address\Total $total
*
@return array
*/
public function fetch(
\Magento\Quote\Model\Quote $quote,
\Magento\Quote\Model\Quote\Address\Total $total
) {
return [
'code' => $this->getCode(),
'title' => 'Custom Total',
'value' => 150
];
}
/**
*
@return \Magento\Framework\Phrase
*/
public function getLabel()
{
return __('Custom Total');
}
}
<module_dir>/view/frontend/layout/checkout_cart_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation=" urn:magento:framework:View/Layout/etc/page_configura
tion.xsd" >
<body>
<referenceBlock name="checkout.cart.totals" >
<arguments>
<argument name="jsLayout" xsi:type="array"
>
<item name="components" xsi:type=
"array" >
<item name =
"block-totals" xsi:type="array">
<item name=
"children" xsi:type="array">
<item
name=
"custom_total" x si:type=
"array">
<item
name=
"component"
xsi:type= "string">Vendor_Module/js/view/checkout/cart/totals/custom_total </item>
<item
name=
"sortOrder"
xsi:type= "string">20</item>
<item
name=
"config"
xsi:type=
"array" >
<item
=
name "template"
xsi:type= "string">Vendor_Module/checkout/cart/totals/custom_total </item>
<item
=
name "title"
xsi:type= >
"string" Custom
Total</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout =" 1column"
xsi:noNamespaceSchemaLocation=" urn:magento:framework:View/Layout/etc/page_configura
tion.xsd" >
<body>
<referenceBlock name="checkout.root" >
<arguments>
<argument name="jsLayout" xsi:type =
"array" >
<item name="components" xsi:type ="array" >
<item name=
"checkout" xsi:type =
"array" >
<item name=
"children" xsi:type ="array" >
<item
name=
"sidebar" xsi:type =
"array" >
<item
name=
"children" x si:type ="array" >
<item
name=
"summary" xsi:type =
"array">
<item
name =
"children" x si:type =
"array"
>
<item
name="totals"
xsi:type= "array">
<item
name ="children"
xsi:type= "array">
<item
name ="custom_total"
xsi:type= "array">
<item
name ="component"
xsi:type= "string">Vendor_Module/js/view/checkout/cart/totals/custom_total </item>
<item
name ="sortOrder"
xsi:type= "string">20</item>
<item
name ="config"
xsi:type= "array">
<item
name="template"
xsi:type= "string">Vendor_Module/checkout/cart/totals/custom_total </item>
<item
name=
"title"
xsi:type= "string">Custom Total</item>
</item>
</item>
</item>
</item>
<item
name="cart_items"
xsi:type= "array">
<item
name ="children"
xsi:type= "array">
<item
name ="details"
xsi:type= "array">
We can also create the components themselves, together with HTML templates:
<module_dir>/view/frontend/web/js/view/checkout/cart/totals/custom_total.js
define(
[
'Vendor_Module/js/view/checkout/summary/custom_total'
],
function (Component) {
'use strict';
return Component.extend({
/**
* @override
*/
<module_dir>/view/frontend/web/template/checkout/cart/totals/custom_total.html
<module_dir>/view/frontend/web/js/view/checkout/summary/custom_total.js
define(
[
Magento_Checkout/js/view/summary/abstract-total',
'
'Magento_Checkout/js/model/quote',
'Magento_Catalog/js/price-utils',
'Magento_Checkout/js/model/totals'
],
function (Component, quote, priceUtils, totals) {
"use strict";
return Component.extend({
defaults: {
isFullTaxSummaryDisplayed:
window.checkoutConfig.isFullTaxSummaryDisplayed || false,
isDisplayed: function() {
return this.isFullMode() && this.getPureValue() !==
0;
},
getValue: function() {
var price = 0;
if (this.totals()) {
price = totals.getSegment('custom_total').value;
}
return this.getFormattedPrice(price);
},
getPureValue: function() {
var price = 0;
if (this.totals()) {
price = totals.getSegment('custom_total').value;
}
return price;
}
});
}
);
getValue and getPureValue methods return the values of our custom total, but getValue
method formats the value, adding two decimal digits and the actual currency symbol.
<module_dir>/view/frontend/web/template/checkout/summary/custom_total.html
Order Emails
To add a new total display into order email, add a new block in sales_email_order_items
layout.
<module_dir>/view/frontend/layout/sales_email_order_items.xml
<module_dir>/Block/Order/CustomTotal.php
<?php
namespace Vendor\Module\Block\Order;
class CustomTotal extends
\Magento\Framework\View\Element\AbstractBlock
{
Log in to the admin panel and navigate to Marketing -> Cart Price Rules. Press “Add
New Rule” button and select the rule name, relationship with store and customer
groups, conditions for discount and the coupon relevance.
<?php
....
protected $ruleFactory
public
function __construct(
\Magento\SalesRule\Model\RuleFactory $ruleFactory
) {
$this->rulesFactory = $ruleFactory
}
<?php
...
$ruleData = [
"name" => "Custom Cart Rule",
"description" => "Buy some products and get one more
free",
"from_date" => null,
"to_date" => null,
"uses_per_customer" => "0",
"is_active" => "1",
"stop_rules_processing" => "0",
"is_advanced" => "1",
"product_ids" => null,
"sort_order" => "0",
"simple_action" => "buy_x_get_y",
"discount_amount" => "1.0000",
"discount_qty" = >null,
$ruleModel = $this->ruleFactory->create();
$ruleModel->setData($ruleData);
$ruleModel->save();
<?php
....
protected $ruleFactory
protected $productFoundConditionFactory;
protected $productConditionFactory;
public
function __construct(
\Magento\SalesRule\Model\RuleFactory $ruleFactory,
\Magento\SalesRule\Model\Rule\Condition\Product\FoundFactory
$productFoundConditionFactory,
...
$discount = '25';
$sku =
'PRODUCT_SKU';
$shoppingCartPriceRule = $this->rulesFactory->create();
$shoppingCartPriceRule->setName('25% off with multiple products - ' .
$sku)
->setDescription('Get 25% off with two or more products)
->setFromDate('2000-01-01')
->setToDate(NULL)
->setUsesPerCustomer('0')
->setCustomerGroupIds(array('0','1','2','3',))
->setIsActive('1')
->setStopRulesProcessing('0')
->setIsAdvanced('1')
->setProductIds(NULL)
->setSortOrder('1')
->setSimpleAction('by_percent')
->setDiscountAmount($discount)
->setDiscountQty(NULL)
->setDiscountStep('0')
->setSimpleFreeShipping('0')
->setApplyToShipping('0')
->setTimesUsed('0')
->setIsRss('0')
->setWebsiteIds(array('1',))
->setCouponType('1')
$productFoundCondition =
$this->productFoundConditionFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product\Found')
->setValue(1) // 1 == FOUND
->setAggregator('all'); // match ALL conditions
$productCondition = $this->productConditionFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product')
->setAttribute('sku')
->setOperator('==')
->setValue($sku);
$productFoundCondition->addCondition($productCondition);
$shoppingCartPriceRule->getConditions()->addCondition($productFoundCo
ndition);
$skuCondition = $this->productConditionFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product')
->setAttribute('sku')
->setOperator('==')
->setValue($sku);
$shoppingCartPriceRule->getActions()->addCondition($skuCondition);
$qtyCondition = $this->productConditionFactory->create()
->setType('Magento\SalesRule\Model\Rule\Condition\Product')
->setAttribute('quote_item_qty')
->setOperator('>=')
->setValue('2');
$shoppingCartPriceRule->getActions()->addCondition($qtyCondition);
$shoppingCartPriceRule->save();
For each customer, there can be only one active coupon. Rules cannot add other
products to the cart and apply to the item they were assigned to
work with.
Cart rules can slow down the process of adding a product to the cart, as well as
checkout and shopping cart pages speed. Performance impact from cart rules can
increase in case there is a large number of cart rules with a wide area of application
(without connection to website or customer group) and without coupons.
● Reorder:
Action: \Magento\Sales\Controller\AbstractController\Reorder
Available events: checkout_cart_product_add_after, sales_quote_product_add_after,
sales_quote_add_item
vendor/magento/module-configurable-product/view/frontend/layout/checkout_cart_ite
m_renderers.xml
<?xml version="1.0"?>
<!--
/**
* Copyright (c) Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/
page_configuration.xsd">
<body>
<referenceBlock name="checkout.cart.item.renderers">
<block
class="Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configura
ble" as
="configurable"
template="Magento_Checkout::cart/item/default.phtml">
<block
public
function getItemHtml(\Magento\Quote\Model\Quote\Item $item)
{
$renderer =
$this->getItemRenderer($item->getProductType())->setItem($item);
return $renderer->toHtml();
Therefore, the product will be available in the template of our block by calling $product
= $block->getItem();
<module_dir>/view/frontend/layout/checkout_cart_item_renderers.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/
page_configuration.xsd">
<body>
<referenceBlock name="checkout.cart.item.renderers">
<block class="\Vendor\Module\Block\Cart\CustomRenderer"
as="custom-type"
template="Magento_Checkout::cart/item/default.phtml">
<block
class="Magento\Checkout\Block\Cart\Item\Renderer\Actions"
name="checkout.cart.item.renderers.custom.actions" as="actions">
<block
class="Magento\Checkout\Block\Cart\Item\Renderer\Actions\Edit"
name="checkout.cart.item.renderers.custom.actions.edit"
template="Magento_Checkout::cart/item/renderer/actions/edit.phtml"/>
<block
class="Magento\Checkout\Block\Cart\Item\Renderer\Actions\Remove"
name="checkout.cart.item.renderers.custom.actions.remove"
template="Magento_Checkout::cart/item/renderer/actions/remove.phtml"/
>
</block>
</block>
</referenceBlock>
</body>
</page>
Afterwards, all the “custom-type” type products will use our class for display.
<module_dir>/etc/events.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events
.xsd">
<event name="sales_quote_remove_item">
<observer name="removeCartItem"
instance="Vendor\Module\Observer\RemoveCartItem" />
<module_dir>/Observer/RemoveCartItem.php
namespace Vendor\Module\Observer;
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extensio
n_attributes.xsd">
<extension_attributes
for="Magento\Quote\Api\Data\AddressInterface">
<attribute code="custom_field" type="string" />
</extension_attributes>
</config>
<module_dir>/adminhtml/system.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="title" translate="label" type="text"
sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Title</label>
</field>
<field id="name" translate="label" type="text"
sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Method Name</label>
</field>
<field id="price" translate="label" type="text"
sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Price</label>
<validate>validate-number
validate-zero-or-greater</validate>
</field>
<field id="sort_order" translate="label" type="text"
sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Sort Order</label>
</field>
<field id="showmethod" translate="label"
type="select" sortOrder="92" showInDefault="1" showInWebsite="1"
showInStore="0">
<label>Show Method if Not Applicable</label>
● price;
● name;
● showmethod– whether the shipping method is displayed even if it can not be
applied to the actual cart / customer.
Config.xml file sets default values for the parameters from system.xml file.
<module_dir>/etc/config.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/c
onfig.xsd">
<default>
<carriers>
<customshipping>
<active>1</active>
<model>
Vendor\Module\Model\Carrier\CustomShipping</model>
<name>Custom Shipping</name>
<price>5.00</price>
<title>Custom Shipping</title>
When configuration files are created, we create our shipping method model:
<module_dir>/Model/Carrier/CustomShipping.php
<?php
namespace Vendor\Module\Model\Carrier;
class CustomShipping
extends \Magento\Shipping\Model\Carrier\AbstractCarrier
implements \Magento\Shipping\Model\Carrier\CarrierInterface
{
/**
* Constant defining shipping code for method
*/
const SHIPPING_CODE = 'customshipping';
/**
* @var string
*/
protected $_code = self::SHIPPING_CODE;
/**
* @var \Magento\Shipping\Model\Rate\ResultFactory
*/
protected $rateResultFactory;
/**
* @var
\Magento\Quote\Model\Quote\Address\RateResult\MethodFactory
*/
/**
* @param \Magento\Framework\App\Config\ScopeConfigInterface
$scopeConfig
* @param
\Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory
$rateErrorFactory
* @param \Psr\Log\LoggerInterface $logger
* @param \Magento\Shipping\Model\Rate\ResultFactory
$rateResultFactory
* @param
\Magento\Quote\Model\Quote\Address\RateResult\MethodFactory
$rateMethodFactory
* @param array $data
*/
public function __construct(
\Magento\Framework\App\Config\ScopeConfigInterface
$scopeConfig,
\Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory
$rateErrorFactory,
\Psr\Log\LoggerInterface $logger,
\Magento\Shipping\Model\Rate\ResultFactory
$rateResultFactory,
\Magento\Quote\Model\Quote\Address\RateResult\MethodFactory
$rateMethodFactory,
array $data = []
) {
$this->rateResultFactory = $rateResultFactory;
$this->rateMethodFactory = $rateMethodFactory;
parent::__construct($scopeConfig, $rateErrorFactory, $logger,
$data);
}
/**
* @return array
*/
/**
* @param \Magento\Quote\Model\Quote\Address\RateRequest
$request
* @return bool|\Magento\Shipping\Model\Rate\Result
*/
public function
collectRates(\Magento\Quote\Model\Quote\Address\RateRequest
$request)
{
if (!$this->getActiveFlag()) {
return false;
}
** @var \Magento\Quote\Model\Quote\Address\RateResult\Method
/
$method */
$method = $this->rateMethodFactory->create();
$method->setCarrier(self::SHIPPING_CODE);
// Get the title from the configuration, as defined in
system.xml
$method->setCarrierTitle($this->getConfigData('title'));
$method->setMethod(self::SHIPPING_CODE);
// Get the title from the configuration, as defined in
system.xml
$method->setMethodTitle($this->getConfigData('name'));
$method->setPrice($amount);
$method->setCost($amount);
$result->append($method);
return $result;
}
}
$_code parameters in this model should contain a unique shipping method code; in our
case, it’s “customshipping”.
$this->getConfigData method allows to get the configuration values for our method
(based on the available in system.xml file).
collectRates method can be applied to check the availability of the given method for a
certain customer. For instance, if we need to limit the method availability to certain
postcodes only, this check should be performed there.
This method can be used to set up shipping methods. The call is performed in
collectCarrierRates method of the \Magento\Shipping\Model\Shipping class.
You can create a plugin for either placeOrder or submitQuote function from
Magento\Quote\Model\QuoteManagement class or create an observer for each of the
events:
sales_model_service_quote_submit_before
sales_model_service_quote_submit_success
sales_model_service_quote_submit_failure
checkout_submit_before
checkout_submit_all_after
This allows to integrate a custom logic into the order creation process (for example,
sending the data to the third-party ERP system side).
State can have several statuses which allows to describe the order process more
flexibly. The connection between state and status is stored in sales_order_status_state
table.
$order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING);
$order->setStatus('processing');
$order->addStatusToHistory($order->getStatus(), 'Custom Message');
$order->save();
You can create a new status via the admin panel (navigate to Stores -> Order Status) or
via setup script
(\Magento\Sales\Setup\Patch\Data\InstallOrderStatusesAndInitialSalesConfig class).
Adding a new state is possible only via setup script.
It can also have two types - online invoice and offline invoice.
Online invoice calls the capture method for payment method, which, in its turn, sends a
query to the payment system. Offline invoice modifies the payment information only on
Magento side.
Credit Memo is responsible in Magento 2 for the refund, allowing to return the order
partially or completely. Also, refund can be offline and online (depending on the order
type). The difference between offline and online lies in the following: offline refund is
performed on Magento side and does not send any requests to the payment processing
system, while online refund sends a query to the payment system.
/ attempt to void
/
if ($isOnline) {
$method = $this->getMethodInstance();
$method->setStore($order->getStoreId());
$method->{$gatewayCallback}($this);
}
Magento 2 Enterprise Edition also allows to perform the refund into Store Credits, with
which a customer can later pay for other items in this store.
<referenceBlock name="customer_account_navigation">
<block class="Magento\Customer\Block\Account\SortLinkInterface"
name="customer-account-navigation-address-link">
<arguments>
<argument name="label" xsi:type="string"
translate="true">My awesome menu item</argument>
<argument name="path"
xsi:type="string">path/i/need</argument>
As a parameter, we need to pass the name of the block we want to delete. For example,
let us delete the newsletter subscriptions page:
Finally, when we need to change the block order, we can modify the sortOrder argument
value. For example, let us make the Wishlist section the first menu item:
<referenceBlock name="customer-account-navigation-wish-list-link">
<arguments>
<argument name="sortOrder" xsi:type="number">500</argument>
</arguments>
</referenceBlock>
<referenceContainer name="sales.order.history.info">
<block class="Magento\Framework\View\Element\Text" name="my_text">
<arguments>
<argument name="text" xsi:type="string">Hey! I have been
added to demonstrate the ability to adding the new blocks!</argument>
</arguments>
</block>
</referenceContainer>
As a result, on the My Orders page in the user account we will see the following text:
To create a new customer attribute, use setup scripts. This is the example of customer
attribute creation:
...
class InstallData implements
\Magento\Framework\Setup\InstallDataInterface
{
private $customerSetupFactory;
private $attributeSetFactory;
public function __construct(
\Magento\Customer\Setup\CustomerSetupFactory $customerSetupFactory,
\Magento\Eav\Model\Entity\Attribute\SetFactory $attributeSetFactory
) {
$this->customerSetupFactory = $customerSetupFactory;
$this->attributeSetFactory = $attributeSetFactory;
$customerSetup->addAttribute(\Magento\Customer\Model\Customer::ENTITY
, 'new_attribute', [
'type' => 'varchar',
'label' => 'new attribute',
'input' => 'text',
'required' => false,
'visible' => true,
'user_defined' => true,
'system' => false,
'used_in_forms' => [
'adminhtml_customer',
'adminhtml_checkout',
'checkout_register',
'customer_account_create',
'customer_account_edit',
]
]);
}
...
Customer attributes can be displayed in different forms, which are set in used_in_forms
parameter. The list of forms can be found in customer_form_attribute table.
Module developers can not modify API Data interfaces, described in core Magento 2,
but the majority of the modules have the extension attributes mechanism. Extension
attributes are set and stored separately from the data object of the initial class, so
everything, connected with data storage and extraction, must be realized by the
developer himself.
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extensio
n_attributes.xsd"> <extension_attributes
for="Magento\Customer\Api\Data\CustomerInterface"> <attribute
To get or modify extension attributes, apply the system of plugins to save, get, getList
methods for Product Repository.
File etc/extension_attributes.xml:
<?xml version="1.0" encoding="utf-8" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extensio
n_attributes.xsd">
<extension_attributes
for="Magento\Customer\Api\Data\CustomerInterface">
<attribute code="ext_customer_attribute" type="string" />
</extension_attributes>
</config>
etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/et
c/config.xsd">
<type name="Magento\Customer\Api\CustomerRepositoryInterface">
<plugin name="extensionAttributeExtCustomerAttribute"
type="Belvg\CustomExtAttribute\Model\Plugin\CustomerRepository" />
</type>
</config>
Model/Plugin/CustomerRepository.php
<?php
namespace Belvg\CustomExtAttribute\Model\Plugin\CustomerRepository;
...
$extensionAttributes->setExtCustomerAttribute($extCustomerAttribute)
return $result;
}
$customerSetup->addAttribute(\Magento\Customer\Api\AddressMetadataInt
erface::ENTITY_TYPE_ADDRESS, 'custom_address_attribute', [
'type' => 'varchar',
'label' => 'Custom address attribute',
'input' => 'text',
'required' => false,
'visible' => true,
'user_defined' => true,
'sort_order' => 100,
'position' => 100,
'system' => 0,
]);
$customerEntity =
$customerSetup->getEavConfig()->getEntityType(\Magento\Customer\Api\A
ddressMetadataInterface::ENTITY_TYPE_ADDRESS);
$attributeSetId =
$customerEntity->getDefaultAttributeSetId();
$attributeSet = $this->attributeSetFactory->create();
$attributeGroupId =
$attributeSet->getDefaultGroupId($attributeSetId);
$attribute =
$customerSetup->getEavConfig()->getAttribute(\Magento\Customer\Api\Ad
dressMetadataInterface::ENTITY_TYPE_ADDRESS,
'district');
$attribute->addData([
'attribute_set_id' => $attributeSetId,
'attribute_group_id' => $attributeGroupId,
'used_in_forms' =>
[
'adminhtml_customer_address',
$attribute->save();
User groups allow to modify taxes and discounts, create separate price rules for
different product groups, as well as separate their rights at the store side. Different
product groups have different cache for blocks.
NOT LOGGED IN is the only user group that you can not delete (same as you can not
delete the default registered users group, but, in contrast to NOT LOGGED IN, it is not
static, which means it can be modified). NOT LOGGED IN is assigned to all the visitors
without a session and determines the type of shopping cart (guest cart).
General is the default group for the unlogged users.
Stores > Configuration > General > General > Store Information
VAT Number - this is where you set the seller’s VAT number.
Default Value for Disable Automatic Group Changes Based on VAT ID - when selected, it
automatically changes the user group after the VAT Number is validated.
The parameters you see at the screenshot below are responsible for group selection
after the validation, if the “Default Value for Disable Automatic Group Changes Based on
VAT ID” parameter is active.