Understanding the Magento Architecture
Building web shops is a challenging and tedious job, and even more so if a platform you are working on is limited via features, extensibility, and the overall ecosystem it provides. Choosing the right platform can often make the difference between a project's success or failure. The abundance of available e-commerce software, from SaaS to self-hosted solutions, does not really make it an easy choice.
The Magento e-commerce platform has been around for over 10 years now. With its first stable release dating back to March 2008, it immediately caught the attention of developers as an extensible and feature-rich open source platform. Over time, Magento established itself as not just a stunning technical and feature-rich platform, but as a robust ecosystem as well. By allowing developers to validate their real-world skills through the Magento certification program, certain standards have been put into effect, making it easier for merchants to better recognize their solution partners. Training courses have been further provided for other roles in e-commerce business as well, such as merchants, marketers, system administrators, and business analysts.
In this chapter, we will take a look at some of the key must-knows about Magento:
- Installing Magento
- Modes
- Areas
- Request flow processing
- Modules
- Cache
- Dependency injection
- Plugins
- Events and observers
- Console commands
- Cron jobs
To keep things compact as we move forward, let's assume the following throughout this book:
- We are working on the magelicious.loc project
- We are referring to our project root directory as <PROJECT_DIR>
- We are referring to the <PROJECT_DIR>/app/code/Magelicious directory as <MAGELICIOUS_DIR>
- We are referring to Magento's vendor/magento directory as <MAGENTO_DIR>
- We have a running LAMP/MAMP/WAMP stack (Apache, MySQL, PHP) that is compliant with Magento's requirements
- We have a Composer package manager installed
- We have access to crontab (Linux, MacOS) or Task Scheduler (Windows)
Technical requirements
You will need to have basic knowledge of PHP, OOP, JavaScript, and XML. You will also need Apache, MySQL, and AMPPS installed on your system to execute the codes.
The code files of this chapter can be found on GitHub:
https://github.com/PacktPublishing/Magento-2-Quick-Start-Guide.
Check out the following video to see the Code in Action:
Installing Magento
The Magento platform comes in two flavors:
- Magento Open Source: The free version, targeting small businesses
- Magento Commerce: The commercial version, targeting small, medium, or enterprise businesses
The difference between the two comes mainly in the form of extra modules that were added to the Commerce version, whereas all the coding concepts and core features remain the same. It goes to say that any knowledge we obtain through following Magento Open Source examples is fully applicable to anyone working on Magento Commerce.
There are several ways that we can obtain source files for Magento Open Source:
- Source file archive (.zip, .tar.gz, .tar.bz2), available at https://magento.com
- Git repository, available at https://github.com/magento/magento2
- Composer repository, available at https://repo.magento.com
Obtaining source files via a CLI from the composer repository is our preferred method. Assuming we are within the empty <PROJECT_DIR> directory, we can kick off this process via the following command:
composer create-project --repository-url=https://repo.magento.com/ magento/project-community-edition .
The dot (.) at the end of this command this tells the composer to pull the files into a current directory.
Once the Composer process is finished, we can start installing Magento. There are two ways we can install Magento:
- Via the Web Setup Wizard: The graphical, browser-based process
- Via the command line: The command-line-based process
Knowing how to install Magento via the command line is an essential skill in day-to-day development, as the majority of development requires the developer to tackle various bin/magento commands—not to mention the command line approach is somewhat faster and easily scripted.
Let's install Magento with the built-in php bin/magento setup:install command and a few of the required installation options as follows:
php bin/magento setup:install \
--db-host="/Applications/MAMP/tmp/mysql/mysql.sock" \
--db-name=magelicious \
--db-user=root
--db-password=root \
--admin-firstname=John \
--admin-lastname=Doe \
--admin-email=john@magelicious.loc \
--admin-user=john \
--admin-password=jrdJ%0i9a69n
After the preceding command has been executed, we should begin to see console progress, starting with something like the following:
Starting Magento installation:
File permissions check...
[Progress: 1 / 513]
Required extensions check...
[Progress: 2 / 513]
Enabling Maintenance Mode...
[Progress: 3 / 513]
Installing deployment configuration...
[Progress: 4 / 513]
Installing database schema:
Schema creation/updates:
Module 'Magento_Store':
[Progress: 5 / 513]
While it might take up to a few minutes, a successful installation should end with a message that's similar to the following:
[Progress: 508 / 513]
Installing admin user...
[Progress: 509 / 513]
Caches clearing:
Cache cleared successfully
[Progress: 510 / 513]
Disabling Maintenance Mode:
[Progress: 511 / 513]
Post installation file permissions check...
For security, remove write permissions from these directories: '/Users/branko/Projects/magelicious/app/etc'
[Progress: 512 / 513]
Write installation date...
[Progress: 513 / 513]
[SUCCESS]: Magento installation complete.
[SUCCESS]: Magento Admin URI: /admin_mxq00c
Nothing to import.
Right after installation, our first step should be to set Magento to developer mode by using the following command:
php bin/magento deploy:mode:set developer
We will take a closer look at Magento modes soon; for now, this is to be taken as is.
If all went well during the Magento installation, we should be able to open the storefront and admin in our browser.
Modes
Modes play a crucial role in Magento's development and deployment processes. They are handled by the deploy module, which can be found under the <MAGENTO_DIR>/module-deploy directory.
The built-in php bin/magento command provides us with the following deploy commands:
deploy
deploy:mode:set Set application mode.
deploy:mode:show Displays current application mode.
We already used the deploy:mode:set developer command to switch from default to developer mode.
Magento differentiates between following three modes:
- default: The default after-install mode:
- Not optimized for production
- Symlinks to static view files are published to the pub/static directory
- Errors and exceptions are not shown to the user, as they are logged to the filesystem
- Should avoid using it
- developer: For development systems only:
- Symlinks to static view files are published to the pub/static directory
- Provides verbose logging
- Enables automatic code compilation
- Enables enhanced debugging
- Slowest performance
- production: For production systems:
- Errors and exceptions are not shown to the user, as they are logged to the filesystem
- Static view files are not materialized, as they are served from the cache only
- Automatic code file compilation is disabled, as new or updated files are not written to the filesystem
- Enabling and disabling the cache types is not possible from the Magento admin
- Fastest performance
Areas
The area is a logical component that organizes code for optimized request processing. While the majority of the time we don't really have to code anything specific regarding areas, understanding them is key to understanding Magento.
The Magento\Framework\App\Area class AREA_* constants hint at the following areas:
const AREA_GLOBAL = 'global';
const AREA_FRONTEND = 'frontend';
const AREA_ADMINHTML = 'adminhtml';
const AREA_DOC = 'doc';
const AREA_CRONTAB = 'crontab';
const AREA_WEBAPI_REST = 'webapi_rest';
const AREA_WEBAPI_SOAP = 'webapi_soap';
By doing a lookup for the <argument name="areas" string across all of the <MAGENTO_DIR> di.xml files, we can see that five of these areas have been explicitly added to the areas argument of the Magento\Framework\App\AreaList class:
- adminhtml via <MAGENTOI_DIR>/module-backend/etc/di.xml
- webapi_rest via <MAGENTOI_DIR>/module-webapi/etc/di.xml
- webapi_soap via <MAGENTOI_DIR>/magento/module-webapi/etc/di.xml
- frontend via <MAGENTOI_DIR>/magento/module-store/etc/di.xml
- crontab via <MAGENTOI_DIR>/magento/module-cron/etc/di.xml
The default area is frontend, as defined by the default argument under module-store/etc/di.xml. The global area is used as a fallback for files that are absent in the adminhtml and frontend areas.
Let's take a closer look at the <MAGENTO_DIR>/module-webapi/etc/di.xml file:
<type name="Magento\Framework\App\AreaList">
<arguments>
<argument name="areas" xsi:type="array">
<item name="webapi_rest" xsi:type="array">
<item name="frontName" xsi:type="string">rest</item>
</item>
<item name="webapi_soap" xsi:type="array">
<item name="frontName" xsi:type="string">soap</item>
</item>
</argument>
</arguments>
</type>
The frontName is what sometimes appears at the front of the URL, whereas the area name is used internally to refer to the area in configuration files. Different areas defined by Magento can contain different code for processing URLs and requests. This allows Magento to load only the dependent code for the specified area.
When developing modules, we define which resources are visible and accessible in a given area. This way, we get to control the specific area behavior if needed. An example of one such behavior might be the definition of the event observer under the frontend area for customer_save_after event. This observer would only trigger on customer save operations that are triggered from the storefront, which usually indicates a customer register action. The adminhtml area operations, such as Magento admin manually creating a customer, would fail to trigger this observer, as it was defined under the frontend area.
On occasion, we might need to run some code that only executes under certain areas. In such cases, emulation helps us emulate any store programmatically. The Magento\Store\Model\App\Emulation class provides the startEnvironmentEmulation and stopEnvironmentEmulation methods, which we can use for this purpose, as per the following partial example:
protected $storeRepository;
protected $emulation;
public function __construct(
\Magento\Store\Api\StoreRepositoryInterface $storeRepository,
\Magento\Store\Model\App\Emulation $emulation
) {
$this->storeRepository = $storeRepository;
$this->emulation = $emulation;
}
public function test() {
$store = $this->storeRepository->get('store-to-emulate');
$this->emulation->startEnvironmentEmulation(
$store->getId(),
\Magento\Framework\App\Area::AREA_FRONTEND
);
// Code to execute in emulated environment
$this->emulation->stopEnvironmentEmulation();
}
While it is not a common thing to do, we can further register new areas ourselves. This is easily done by using the module's di.xml.
Request flow processing
URLs in Magento have the format of <AreaFrontName>/<VendorName>/<ModuleName>/<ControllerName>/<ActionName>, but this does not mean that we actually use the area, vendor, or module name in the URL any time we wish to access a certain controller. For example, the area for a request is defined by the first request path segment, such as admin for adminhtml area, and none for frontend area.
We use the router class to assign a URL to a corresponding controller and its action. The router's match method finds a matching controller, which is determined by an incoming request.
Conceptually, creating a new router is as simple as doing the following:
- Inject the new item under the routerList argument of the Magento\Framework\App\RouterList type via the di.xml file.
- Create a router file (by using the match method, which implements \Magento\Framework\App\RouterInterface).
- Return an instance of \Magento\Framework\App\ActionInterface.
By doing a lookup for the name="routerList" string across all of the <MAGENTO_DIR> di.xml files, we can see the following router definitions:
- Magento\Robots\Controller\Router (robots)
- Magento\Cms\Controller\Router (cms)
- Magento\UrlRewrite\Controller\Router (urlrewrite)
- Magento\Framework\App\Router\Base (standard)
- Magento\Framework\App\Router\DefaultRouter (default)
- Magento\Backend\App\Router (admin)
Let's take a closer look at the robots router under <MAGENTO_DIR>/module-robots. etc/frontend/di.xml injects the new item under the routerList argument as follows:
<type name="Magento\Framework\App\RouterList">
<arguments>
<argument name="routerList" xsi:type="array">
<item name="robots" xsi:type="array">
<item name="class" xsi:type="string">Magento\Robots\Controller\Router</item>
<item name="disable" xsi:type="boolean">false</item>
<item name="sortOrder" xsi:type="string">10</item>
</item>
</argument>
</arguments>
</type>
The Magento\Robots\Controller\Router class has been further defined as per the following partial extract:
class Router implements \Magento\Framework\App\RouterInterface {
// Magento\Framework\App\ActionFactory
private $actionFactory;
// Magento\Framework\App\Router\ActionList
private $actionList;
// Magento\Framework\App\Route\ConfigInterface
private $routeConfig;
public function match(\Magento\Framework\App\RequestInterface $request) {
$identifier = trim($request->getPathInfo(), '/');
if ($identifier !== 'robots.txt') {
return null;
}
$modules = $this->routeConfig->getModulesByFrontName('robots');
if (empty($modules)) {
return null;
}
$actionClassName = $this->actionList->get($modules[0], null, 'index', 'index');
$actionInstance = $this->actionFactory->create($actionClassName);
return $actionInstance;
}
}
The match method checks if the robots.txt file was requested and returns the instance of the matched \Magento\Framework\App\ActionInterface type. By following this simple implementation, we can easily create the route of our own.
Conceptually, creating a new controller is as simple as doing the following:
- Register a route via router.xml.
- Create an abstract controller file (as an abstract class, which extends \Magento\Framework\App\Action\Action).
- Create an action controller file (which extends the main controller file with the execute method, and implements \Magento\Framework\App\ActionInterface).
- Return an instance of \Magento\Framework\Controller\ResultInterface.
By doing a lookup for the <route string across the <MAGENTO_DIR> routes.xml files, we can see that Magento uses hundreds of route definitions, which are spread across its modules. Each route represents one controller.
Let's take a closer look at one of Magento's controllers, <MAGENTO_DIR>/module-customer, which maps to the http://magelicious.loc/customer/address/form URL. The route itself is registered via frontend/di.xml under the standard router with a customer ID and a customer frontName, as follows:
<router id="standard">
<route id="customer" frontName="customer">
<module name="Magento_Customer" />
</route>
</router>
The abstract controller file Controller/Address.php is defined partially as follows:
abstract class Address extends \Magento\Framework\App\Action\Action {
// The rest of the code...
}
The abstract controller is where we want to add functionality and dependencies that are shared across all of the child action controllers.
We can further see three different action controllers defined within the subdirectory which has the same name as the abstract class. The Controller/Address directory contains six action controllers: Delete.php, Edit.php, Form.php, FormPost.php, Index.php, and NewAction.php. Let's take a closer look at the following partial Form.php file's content:
class Form extends \Magento\Customer\Controller\Address {
public function execute() {
/** @var \Magento\Framework\View\Result\Page $resultPage */
$resultPage = $this->resultPageFactory->create();
$navigationBlock = $resultPage->getLayout()->getBlock('customer_account_navigation');
if ($navigationBlock) {
$navigationBlock->setActive('customer/address');
}
return $resultPage;
}
}
The example here uses the create method of the injected Magento\Framework\View\Result\PageFactory type to create a new page result. The various types of controller results can be found within the <MAGENTO_DIR>/framework directory:
- Magento\Framework\Controller\Result\Json
- Magento\Framework\Controller\Result\Raw
- Magento\Framework\Controller\Result\Redirect
- Magento\Framework\Controller\Result\Forward
- Magento\Framework\View\Result\Layout
- Magento\Framework\View\Result\Page
We can take the unified way of creating result instances by using the create method of \Magento\Framework\Controller\ResultFactory. The ResultFactory defines the TYPE_* constant for each of the mentioned controller result types:
const TYPE_JSON = 'json';
const TYPE_RAW = 'raw';
const TYPE_REDIRECT = 'redirect';
const TYPE_FORWARD = 'forward';
const TYPE_LAYOUT = 'layout';
const TYPE_PAGE = 'page';
Keeping these constants in mind, we can easily write our action controller code as follows:
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
$resultRedirect->setPath('adminhtml/*/index');
return $resultRedirect;
Modules
The top-level Magento structure is rather simple. When we strip away (seemingly) non-relevant files such as licenses, sample files, and changelogs, what remains looks much like the following:
app/
code/
design/
etc/
config.php
env.php
bin/
composer.json
composer.lock
dev/
generated/
index.php
lib/
phpserver/
pub/
static/
adminhtml/
frontend/
setup/
update/
var/
cache/
log/
page_cache/
view_preprocessed/
pub/
static/
adminhtml/
frontend/
vendor/
composer/
magento/
symfony/
The app/code/<VendorName>/<ModuleName> directory, <MAGELICIOUS_DIR> for short, is where our custom code will reside.
The vendor/magento directory, <MAGENTO_DIR> for short, is where Magento source code resides, as per the following partial listing:
vendor/
magento/
composer/
framework/
language-de_de/
language-en_us/
magento-composer-installer/
magento2-base/
module-catalog/
module-checkout/
theme-adminhtml-backend/
theme-frontend-blank/
theme-frontend-luma/
The individual module directory is where things get interesting. Let's take a quick look at the structure of one of the simpler Magento modules, <MAGENTO_DIR>/module-contact:
Block/
Controller/
etc/
Helper/
i18n/
Model/
Test/
view/
composer.json
LICENSE.txt
LICENSE_AFL.txt
README.md
registration.php
This is by no means the final structure of the individual module. There are other directories the module can define, as we will see as we move forward throughout this book.
Creating the minimal module
Let's create the most minimal module there is in Magento. Our module will be called Core and it will belong to the Magelicious vendor. The formula for determining the directory of custom modules is app/code/<VendorName>/<ModuleName>, or in our case <MAGELICIOUS_DIR>/Core.
We start off by creating the <MAGELICIOUS_DIR>/Core/registration.php file with the following content:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Magelicious_Core',
__DIR__
);
The registration.php file is essentially the entry point of our module. The register method of the Magento\Framework\Component\ComponentRegistrar class provides the ability to statically register components, whereas a component can be more than just a module, as defined via the following constants:
- Magento\Framework\Component\ComponentRegistrar::MODULE
- Magento\Framework\Component\ComponentRegistrar::LIBRARY
- Magento\Framework\Component\ComponentRegistrar::THEME
- Magento\Framework\Component\ComponentRegistrar::LANGUAGE
Next, we will create the <MAGELICIOUS_DIR>/Core/etc/module.xml file with the following content:
<config>
<module name="Magelicious_Core" setup_version="1.0.0">
<sequence>
<module name="Magento_Store"/>
<module name="Magento_Backend"/>
<module name="Magento_Config"/>
</sequence>
</module>
</config>
The name and setup_version attributes of a module element are considered required. The sequence, on the other hand, is optional. We use it to define any potential dependencies around other Magento modules.
Finally, we add composer.json with the following content:
{
"name": "magelicious/module-core",
"description": "The core module",
"require": {
"php": "^7.0.0"
},
"type": "magento2-module",
"version": "1.0.0",
"license": [
"OSL-3.0",
"AFL-3.0"
],
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Magelicious\\Core\\": ""
}
}
}
Magento supports the following composer.json types:
- magento2-module for modules
- magento2-theme for themes
- magento2-language for language packages
- magento2-component for general extensions that do not fit any of the other types
Though composer.json is not required for our custom module to be seen by Magento, it is recommended to add it to any component we are building.
You can trigger module installation by running the php bin/magento module:enable Magelicious_Core command, like so:
$ php bin/magento module:enable Magelicious_Core
The following modules have been enabled:
- Magelicious_Core
To make sure that the enabled modules are properly registered, run 'setup:upgrade'.
Cache cleared successfully.
Generated classes cleared successfully. Please run the 'setup:di:compile' command to generate classes.
Info: Some modules might require static view files to be cleared. To do this, run 'module:enable' with the --clear-static-content option to clear them.
You can run the php bin/magento setup:upgrade command to trigger any install and/or update scripts that need to be triggered:
Cache cleared successfully
File system cleanup:
generated/code/Composer
generated/code/Magento
generated/code/Symfony
Updating modules:
Schema creation/updates:
Module 'Magento_Store':
...
Module 'Magento_CmsUrlRewrite':
Module 'Magelicious_Core':
Module 'Magento_ConfigurableImportExport':
...
Nothing to import.
This finishes our module installation.
To confirm our module was installed correctly, perform the following:
- Check the <PROJECT_DIR>/app/etc/config.php file for the 'Magelicious_Core' => 1 entry
- Check the setup_module table for the Magelicious_Core 1.0.0 1.0.0 entry
At the moment, our module does absolutely nothing, aside from just sitting there. However, these few simple steps are the basis for us moving forward with Magento development, because the majority of things in Magento are done via a module, alongside other types of components, which we have already mentioned.
Cache
Magento makes extensive use of caching. The System | Tools | Cache Management section enables us to Enable | Disable | Refresh the cache from the comfort of the graphical interface. During development, the use of the console is more convenient and faster.
The following cache-related commands are supported:
cache
cache:clean Cleans cache type(s)
cache:disable Disables cache type(s)
cache:enable Enables cache type(s)
cache:flush Flushes cache storage used by cache type(s)
cache:status Checks cache status
Out of the box, Magento Open Source comes with 14 different cache types. We can easily get the status of each cache type by running the php bin/magento cache:status command, which gives the following output:
Current status:
config: 0
layout: 0
block_html: 0
collections: 0
reflection: 0
db_ddl: 0
eav: 0
customer_notification: 0
the_custom_cache: 1
config_integration: 0
config_integration_api: 0
full_page: 0
translate: 0
config_webservice: 0
We can use the enable | disable | clean cache commands to impact one or more cache types at once.
If built-in cache types are not enough, we can always create our own.
Creating a new cache type in Magento is as easy as doing the following:
Create the <MAGELICIOUS_DIR>/Core/etc/cache.xml file with the following content:
<config>
<type name="the_custom_cache" translate="label,description" instance="Magelicious\Core\Model\Cache\TheCustomCache">
<label>The Custom Cache</label>
<description>Our custom cache type</description>
</type>
</config>
Create the <MAGELICIOUS_DIR>/Core/Model/Cache/TheCustomCache.php file with the following content:
class TheCustomCache extends \Magento\Framework\Cache\Frontend\Decorator\TagScope {
const TYPE_IDENTIFIER = 'the_custom_cache';
const CACHE_TAG = 'THE_CUSTOM_CACHE';
public function __construct(\Magento\Framework\App\Cache\Type\FrontendPool $cacheFrontendPool) {
parent::__construct($cacheFrontendPool->get(self::TYPE_IDENTIFIER), self::CACHE_TAG);
}
}
The TYPE_IDENTIFIER is used internally as a cache type code that is unique among all cache types. The CACHE_TAG is a cache tag that's used to distinguish the cache type from all other caches. Running cache:status should now show our custom cache type on the list.
We can use the instance of Magento\Framework\App\Cache\TypeListInterface to invalidate the cache, as follows:
$this->typeList->invalidate(\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER);
We can use the instance of Magento\Framework\App\Cache\Manager $cacheManager to programmatically execute the same enable | disable | clean operations as per the following example:
$cacheManager->setEnabled(
[\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER],
true
);
$cacheManager->clean([\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER]);
$cacheManager->flush([\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER]);
Saving data to cache requires serialization, as per the following example:
// \Magento\Framework\Config\CacheInterface $cache
// \Magento\Framework\Serialize\SerializerInterface $serializer
// \Magento\Framework\App\Cache\StateInterface $cacheState
$isCacheEnabled = $cacheState->isEnabled(\Magelicious\Core\Model\Cache\TheCustomCache::TYPE_IDENTIFIER);
$cacheId = 'some-unique-identifier';
if ($isCacheEnabled) {
$cache->save(
$serializer->serialize('some-data'),
$cacheId,
[
\Magelicious\Core\Model\Cache\TheCustomCache::CACHE_TAG
]
);
}
Reading data from the cache is as easy as per the following example:
if ($cacheData = $this->cache->load($cacheId);) {
$someData = $this->getSerializer()->unserialize($cacheData);
} else {
$someData = $this->fetchSomeData();
}
Dependency injection
Dependency injection has become a de facto standard of modern-day software. Magento makes heavy use of this technique, based on mappings found in di.xml files. The workload of Magento's dependency injection is handled by the Magento\Framework\ObjectManager\ObjectManager instance, which implements the lightweight Magento\Framework\ObjectManagerInterface.
The di.xml file configures the object manager, telling it how to handle the following:
- Argument injection
- Virtual types
- Proxies
- Factories
- Plugins
These features allow for a great degree of flexibility and extensibility, as we will soon see.
Every module can have a global and area-specific di.xml file.
Magento loads configuration files in the following order:
- Initial (app/etc/di.xml)
- Global (<ModuleDir>/etc/di.xml)
- Area-specific (<ModuleDir>/etc/<area>/di.xml)
When Magento reads all of these configuration files, it merges them all together by appending all nodes.
Argument injection
Argument injection is done via preference and type definitions within the di.xml.
By performing a lookup for the <preference string across the entire <MAGENTO_DIR> directory's di.xml files, we can see that Magento uses hundreds of preference definitions, spread across the majority of its modules.
Let's take a quick look at one of the __construct method, of the type Magento\Eav\Model\Attribute\Data\AbstractData:
public function __construct(
\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
\Psr\Log\LoggerInterface $logger,
\Magento\Framework\Locale\ResolverInterface $localeResolver
) {
$this->_localeDate = $localeDate;
$this->_logger = $logger;
$this->_localeResolver = $localeResolver;
}
We can find the preference definitions for these interfaces under the <MAGENTO_DIR>/magento2-base/app/etc/di.xml file:
<preference for="Magento\Framework\Stdlib\DateTime\TimezoneInterface" type="Magento\Framework\Stdlib\DateTime\Timezone" />
<preference for="Psr\Log\LoggerInterface" type="Magento\Framework\Logger\Monolog" />
<preference for="Magento\Framework\Locale\ResolverInterface" type="Magento\Framework\Locale\Resolver" />
Theoretically, we can use the object manager directly, as follows:
class Type {
protected $objectManager;
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager
) {
$this->objectManager = $objectManager;
}
public function example() {
$this->objectManager->create(\Fully\Qualified\Class\Name::class);
$this->objectManager->get(\Fully\Qualified\Class\Name::class);
\Magento\Framework\App\ObjectManager::getInstance()
->create(\Fully\Qualified\Class\Name::class);
\Magento\Framework\App\ObjectManager::getInstance()
->get(\Fully\Qualified\Class\Name::class);
}
}
By doing a lookup for the <type string across the entire <MAGENTO_DIR> directory's di.xml files, we can see that Magento uses over a thousand type definitions, spread across the majority of its modules.
Here is a very simple example, taken from the <MAGENTO_DIR>/module-customer/etc/di.xml file:
<type name="Magento\Customer\Model\Visitor">
<arguments>
<argument name="ignoredUserAgents" xsi:type="array">
<item name="google1" xsi:type="string">Googlebot/1.0 (googlebot@googlebot.com http://googlebot.com/)</item>
<item name="google2" xsi:type="string">Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)</item>
<item name="google3" xsi:type="string">Googlebot/2.1 (+http://www.googlebot.com/bot.html)</item>
</argument>
</arguments>
</type>
Looking into the source of the Magento\Customer\Model\Visitor class, we can see that it has its constructor defined by the $ignoredUserAgents = [] array. Using the type element, the preceding example injects the ignoredUserAgents argument with the given array values.
When configuration files for a given scope get merged, array arguments with the same name get merged into a new array. However, if any new configuration is loaded at a later time, either by a more specific scope or through the code, then any array definitions in the new configuration will replace the loaded configuration instead of merging.
The list of available item type values goes well beyond just a string, and includes the following:
- boolean
- string
- number
- null
- object
- const
- init_parameter
- array
Argument injection often goes hand in hand with virtual types, as we will soon see.
Virtual types
Virtual types are a very neat feature of Magento that allow us to change the arguments of a specific injectable dependency and thus change the behavior of a particular class type.
The <MAGENTO_DIR>/module-checkout/etc/di.xml file provides a simple example of virtualType and its usage:
<virtualType name="Magento\Checkout\Model\Session\Storage" type="Magento\Framework\Session\Storage">
<arguments>
<argument name="namespace" xsi:type="string">checkout</argument>
</arguments>
</virtualType>
<type name="Magento\Checkout\Model\Session">
<arguments>
<argument name="storage" xsi:type="object">Magento\Checkout\Model\Session\Storage</argument>
</arguments>
</type>
The virtualType here (virtually) extends Magento\Framework\Session\Storage by rewriting its constructor's $namespace = 'default' argument to $namespace = 'checkout'. However, this change does not kick in on its own, as the Magento\Checkout\Model\Session\Storage virtual type must be used first. It is used in this case, via a type definition, where the storage argument is replaced entirely by the virtual type.
By doing a lookup for the <virtualType string across the entire <MAGENTO_DIR> directory's di.xml files, we can see that Magento uses hundreds of virtual types across the majority of its modules.
A more complex example of virtual type usage can be found under the Magento_LayeredNavigation module.
The <MAGENTO_DIR>/module-layered-navigation/etc/frontend/di.xml file defines two virtual types, as follows:
<virtualType name="Magento\LayeredNavigation\Block\Navigation\Category" type="Magento\LayeredNavigation\Block\Navigation">
<arguments>
<argument name="filterList" xsi:type="object">categoryFilterList</argument>
</arguments>
</virtualType>
<virtualType name="Magento\LayeredNavigation\Block\Navigation\Search" type="Magento\LayeredNavigation\Block\Navigation">
<arguments>
<argument name="filterList" xsi:type="object">searchFilterList</argument>
</arguments>
</virtualType>
Here, we have two virtual types defined, each changing the filterList argument of the Magento\LayeredNavigation\Block\Navigation class. categoryFilterList and searchFilterList are the names of two other virtual types that are defined in <MAGENTO_DIR>/module-catalog-search/etc/di.xml, as visible here: https://github.com/magento/magento2/blob/2.2/app/code/Magento/CatalogSearch/etc/di.xml.
The Magento\LayeredNavigation\Block\Navigation\Category and Magento\LayeredNavigation\Block\Navigation\Search virtual types are then used in layout files for block definition, as follows:
<!-- view/frontend/layout/catalog_category_view_type_layered.xml -->
<referenceContainer name="sidebar.main">
<block class="Magento\LayeredNavigation\Block\Navigation\Category" ...
</referenceContainer>
<!-- view/frontend/layout/catalogsearch_result_index.xml -->
<referenceContainer name="sidebar.main">
<block class="Magento\LayeredNavigation\Block\Navigation\Search" ...
</referenceContainer>
What this effectively does is tell Magento that, for the category view page and search page, use the virtual type for class, thus instructing it to go through all the argument changes specified in the virtual type.
This is an interesting example as it reveals the potential complexity of using virtual types. Basically, we have one virtual type (Magento\LayeredNavigation\Block\Navigation\Search) changing the single filterList argument of a real type (Magento\LayeredNavigation\Block\Navigation) with another virtual type (categoryFilterList). Likewise, we just learned how the class property of the block element is capable of not just accepting the fully qualified class name, but the virtualType name as well.
Proxies
Proxy classes are used when object creation is expensive and a class' constructor is unusually resource-intensive. To avoid unnecessary performance impact, Magento uses Proxy classes to turn given types into becoming lazy-loaded versions of them.
A quick lookup for the \Proxy</argument> string across all Magento di.xml files reveals over a hundred occurrences of this string. It goes to say that Magento extensively uses proxies across its code.
The type definition under <MAGENTO_DIR>/module-customer/etc/di.xml is a nice example of using proxies:
<type name="Magento\Customer\Model\Session">
<arguments>
<argument name="configShare" xsi:type="object">Magento\Customer\Model\Config\Share\Proxy</argument>
<argument name="customerUrl" xsi:type="object">Magento\Customer\Model\Url\Proxy</argument>
<argument name="customerResource" xsi:type="object">Magento\Customer\Model\ResourceModel\Customer\Proxy</argument>
<argument name="storage" xsi:type="object">Magento\Customer\Model\Session\Storage</argument>
<argument name="customerRepository" xsi:type="object">Magento\Customer\Api\CustomerRepositoryInterface\Proxy</argument>
</arguments>
</type>
If we look at the constructor of the Magento\Customer\Model\Session type, we can see that none of the four arguments (configShare, customerUrl, customerResource, and customerRepository) were declared as Proxy within the PHP file. They where rewritten through di.xml. These Proxy types do not really exist just yet, as the Magento dependency injection (di) compilation process creates them. They are automatically generated under the generated directory.
Once it is compiled, the Magento\Customer\Model\Url\Proxy type can easily be found under the generated/code/Magento/Customer/Model/Url/Proxy.php file. Let's take a partial look at it:
class Proxy extends \Magento\Customer\Model\Url
implements \Magento\Framework\ObjectManager\NoninterceptableInterface {
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager,
$instanceName = '\\Magento\\Customer\\Model\\Url',
$shared = true) {
$this->_objectManager = $objectManager;
$this->_instanceName = $instanceName;
$this->_isShared = $shared;
}
public function __sleep() {
return ['_subject', '_isShared', '_instanceName'];
}
public function __wakeup() {
$this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance();
}
public function __clone() {
$this->_subject = clone $this->_getSubject();
}
protected function _getSubject() {
if (!$this->_subject) {
$this->_subject = true === $this->_isShared
? $this->_objectManager->get($this->_instanceName)
: $this->_objectManager->create($this->_instanceName);
}
return $this->_subject;
}
public function getLoginUrl() {
return $this->_getSubject()->getLoginUrl();
}
public function getLoginUrlParams() {
return $this->_getSubject()->getLoginUrlParams();
}
}
The composition of the Proxy class shows the mechanism by which it wraps around the original Magento\Customer\Model\Url type. This now means that, across Magento, every time the Magento\Customer\Model\Url type is requested, the Magento\Customer\Model\Url\Proxy is going to get passed instead. Unlike the original type's __construct method which might be performance heavy, the autogenerated Proxy's __construct method is a lightweight one. This eliminates possible performance bottlenecks. The _getSubject method is used to instantiate/lazy load the original type whenever any of the original type public methods are called. For example, the call to the getLoginUrl method would go through the proxy.
Every proxy generated by Magento implements Magento\Framework\ObjectManager\NoninterceptableInterface. Though the interface itself is empty, it is used as a marker to identify proxies for which we don't need to generate interceptors (plugins).
When writing custom types, such as Magelicious\Core\Model\Customer, we could easily specify the proxy right there in the constructor:
class Customer {
public function __construct(
\Magento\Customer\Model\Url\Proxy $customerUrl
) {
//...
}
}
This approach, however, is a bad practice. The way to do this properly is to specify __construct with an original Magento\Customer\Model\Url type and then add the di.xml as follows:
<type name="Magelicious\Core\Model\Customer">
<arguments>
<argument name="customerUrl" xsi:type="object">Magento\Customer\Model\Url\Proxy</argument>
</arguments>
</type>
Factories
Factories are classes that create other classes—much like the object manager, except this time we are encouraged to use them directly. Their purpose is to instantiate the non-injectable classes—those that we should not inject directly into __construct. The beauty of using factories is that, most of the time, we don't even have to write them, as they are automatically generated by Magento unless we need to implement some sort of specific behavior for our factory classes.
By doing a lookup for the Factory $ string across the entire <MAGENTO_DIR> directory's *.php files, we can see thousands of factory examples, spread across the majority of Magento's modules.
While a great deal of these factories actually exist, others are automatically generated when needed.
Let's take a quick look at one automatically generated factory, that of Magento\Newsletter\Model\SubscriberFactory, which is used in several Magento modules such as the newsletter, subscriber, and review modules:
class SubscriberFactory {
protected $_objectManager = null;
protected $_instanceName = null;
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager,
$instanceName = '\\Magento\\Newsletter\\Model\\Subscriber'
) {
$this->_objectManager = $objectManager;
$this->_instanceName = $instanceName;
}
public function create(array $data = array()) {
return $this->_objectManager->create($this->_instanceName, $data);
}
}
The autogenerated factory code is essentially just a thin wrapper on top of an object manager create method.
Factories work well with the di.xml preference mechanism, which means we can easily pass interfaces into the constructor, like so:
public function __construct(
\Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository,
\Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory $stockItemCriteriaFactory
) {
$this->stockItemRepository = $stockItemRepository;
$this->stockItemCriteriaFactory = $stockItemCriteriaFactory;
}
// $criteria = $this->stockItemCriteriaFactory->create();
// $result = $this->stockItemRepository->getList($criteria);
The preference mechanism makes sure that concrete implementations get passed to the object instance when its constructor is invoked.
Plugins
Plugins are likely one of the most powerful features of Magento. They allow us to modify the behavior of public class functions by intercepting a function call and running code before, after, or around that function call.
Before we eagerly start using them, it is worth emphasizing how plugins can't be used on the following:
- Final methods
- Final classes
- Non-public methods
- Class methods (such as static methods)
- __construct
- Virtual types
- Objects that are instantiated before Magento or Framework\Interception is bootstrapped
Plugins can be used on the following:
- Classes
- Interfaces
- Abstract classes
- Parent classes
By doing a lookup for the <plugin string across the entire <MAGENTO_DIR> directory's di.xml files, we can see hundreds of plugin examples spread across the majority of Magento's modules.
The before plugin
The before plugin, as its name suggests, runs before the observed method.
When writing a before plugin, there are a few key points to remember:
- The before keyword is appended to the observed instance method. If the observed method is called getSomeValue, then the plugin method is called beforeGetSomeValue.
- The first parameter of the before plugin method is always the observed instance type, often abbreviated as $subject or directly by the class type – which is $processor in our example. We can typecast it for greater readability.
- All other parameters of the plugin method must match the parameters of the observed method.
- The plugin method must return an array with the same type and number of parameters as the observed method's input parameters.
Let's take a look at one of Magento's before plugin implementations, the one specified in the <MAGENTO_DIR>module-payment/etc/frontend/di.xml file:
<type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
<plugin name="ProcessPaymentConfiguration"
type="Magento\Payment\Plugin\PaymentConfigurationProcess"/>
</type>
The original method this plugin is targeting is the process method of the Magento\Checkout\Block\Checkout\LayoutProcessor class:
public function process($jsLayout) {
// The rest of the code...
return $jsLayout;
}
The implementation of the before plugin is provided via the beforeProcess method of the Magento\Payment\Plugin\PaymentConfigurationProcess class, as per the following partial example:
public function beforeProcess(
\Magento\Checkout\Block\Checkout\LayoutProcessor $processor,
$jsLayout) {
// The rest of the code...
return [$jsLayout];
}
The around plugin
The around plugin runs around the observed method in a way that allows us to run some code before and after the original method call. This is a very powerful concept, as we get to change the incoming parameters as well as the return value of a function.
When writing the around plugin, there are a few key points to remember:
- The first parameter coming into the plugin is the observed type instance.
- The second parameter coming into the plugin is a callable/Closure. Usually, this parameter is typed and named as callable $proceed. We must make sure to forward the same parameters to this callable as the original method signature.
- All other parameters are parameters of the original observed method.
- The plugin must return the same value as the original function, ideally return $proceed(…) or $returnValue = $proceed(); followed by $returnValue; for cases where we need to modify the $returnValue.
Let's take a look at one of Magento's around plugin implementations, the one specified in the <MAGENTO_DIR>module-grouped-product/etc/di.xml file:
<type name="Magento\Catalog\Model\ResourceModel\Product\Link">
<plugin name="groupedProductLinkProcessor" type="Magento\GroupedProduct\Model\ResourceModel\Product\Link\RelationPersister" />
</type>
The original method of this plugin is targeting the deleteProductLink method of the Magento\Catalog\Model\ResourceModel\Product\Link class:
public function deleteProductLink($linkId) {
return $this->getConnection()
->delete($this->getMainTable(), ['link_id = ?' => $linkId]);
}
The implementation of the around plugin is provided via the aroundDeleteProductLink method of the Magento\GroupedProduct\Model\ResourceModel\Product\Link\RelationPersister class, as per the following partial example:
public function aroundDeleteProductLink(
\Magento\GroupedProduct\Model\ResourceModel\Product\Link $subject,
\Closure $proceed, $linkId) {
// The rest of the code...
$result = $proceed($linkId);
// The rest of the code...
return $result;
}
The after plugin
The after plugin, as its name suggests, runs after the observed method.
When writing the after plugin, there are a few key points to remember:
- The first parameter coming into the plugin is an observed type instance.
- The second parameter coming into the plugin is the result of the observed method, often called $result or called after the variable returned from the observed method (as in the following example: $data).
- All other parameters are parameters of the observed method.
- The plugin must return the same $result|$data variable of the same type, as we are free to modify the value.
Let's take a look at one of Magento's after plugin implementations, the one specified in the module-catalog/etc/di.xml file:
<type name="Magento\Indexer\Model\Config\Data">
<plugin name="indexerProductFlatConfigGet"
type="Magento\Catalog\Model\Indexer\Product\Flat\Plugin\IndexerConfigData" />
</type>
The original method this plugin is targeting is the get method of the Magento\Indexer\Model\Config\Data class:
public function get($path = null, $default = null) {
// The rest of the code...
return $data;
}
The implementation of the after plugin is provided via the afterGet method of the Magento\Catalog\Model\Indexer\Product\Flat\Plugin\IndexerConfigData class, as per the following partial example:
public function afterGet(Magento\Indexer\Model\Config\Data, $data, $path = null, $default = null) {
// The rest of the code...
return $data;
}
Special care should be taken when using plugins. While they provide great flexibility, they also make it easy to induce bugs, performance bottlenecks, and other less obvious types of instabilities – even more so if several plugins are observing the same method.
Events and observers
Magento has a neat publish-subscribe pattern implementation that we call events and observers. By dispatching events when certain actions are triggered, we can run our custom code in response to the triggered event. The events are dispatched using the Magento\Framework\Event\Manager class, which implements Magento\Framework\Event\ManagerInterface.
To dispatch an event, we simply call the dispatch method of the event manager instance, providing it with the name of the event we are dispatching with an optional array of data we wish to pass on to the observers, as per the following example taken from the <MAGENTO_DIR>/module-customer/Controller/Account/CreatePost.php file:
$this->_eventManager->dispatch(
'customer_register_success',
['account_controller' => $this, 'customer' => $customer]
);
Observers are registered via an events.xml file, as per the following example from the <MAGENTO_DIR>/module-persistent/etc/frontend/events.xml file:
<event name="customer_register_success">
<observer name="persistent" instance="Magento\Persistent\Observer\RemovePersistentCookieOnRegisterObserver" />
</event>
By doing a lookup for the eventManager->dispatch string across the entire <MAGENTO_DIR> directory's *.php files, we can see hundreds of events examples, spread across the majority of Magento's modules. While all of these events are of the same technical importance, we might say that some are likely to be used more on a day to day basis than others.
This makes it worth taking some time to study the following classes and the events they dispatch:
- The Magento\Framework\App\Action\Action class, with the following events:
- controller_action_predispatch
- 'controller_action_predispatch_' . $request->getRouteName()
- 'controller_action_predispatch_' . $request->getFullActionName()
- 'controller_action_postdispatch_' . $request->getFullActionName()
- 'controller_action_postdispatch_' . $request->getRouteName()
- controller_action_postdispatch
- The Magento\Framework\Model\AbstractModel class, with the following events:
- model_load_before
- $this->_eventPrefix . '_load_before'
- model_load_after
- $this->_eventPrefix . '_load_after'
- model_save_commit_after
- $this->_eventPrefix . '_save_commit_after'
- model_save_before
- $this->_eventPrefix . '_save_before'
- model_save_after
- clean_cache_by_tags
- $this->_eventPrefix . '_save_after'
- model_delete_before
- $this->_eventPrefix . '_delete_before'
- model_delete_after
- clean_cache_by_tags
- $this->_eventPrefix . '_delete_after'
- model_delete_commit_after
- $this->_eventPrefix . '_delete_commit_after'
- $this->_eventPrefix . '_clear'
- The Magento\Framework\Model\ResourceModel\Db\Collection class, with the following events:
- core_collection_abstract_load_before
- $this->_eventPrefix . '_load_before'
- core_collection_abstract_load_after
- $this->_eventPrefix . '_load_after'
Some more important events can be found in a few of the types defined under the <MAGENTO_DIR>/framework/View directory:
- view_block_abstract_to_html_before
- view_block_abstract_to_html_after
- view_message_block_render_grouped_html_after
- layout_render_before
- 'layout_render_before_' . $this->request->getFullActionName()
- core_layout_block_create_after
- layout_load_before
- layout_generate_blocks_before
- layout_generate_blocks_after
- core_layout_render_element
Let's take a closer look at one of these events, the one found in the <MAGENTO_DIR>/framework/Model/AbstractModel.php file:
public function afterCommitCallback() {
$this->_eventManager->dispatch('model_save_commit_after', ['object' => $this]);
$this->_eventManager->dispatch($this->_eventPrefix . '_save_commit_after', $this->_getEventData());
return $this;
}
protected function _getEventData() {
return [
'data_object' => $this,
$this->_eventObject => $this,
];
}
The $_eventPrefix and $_eventObject type properties are particularly important here. If we glimpse over types such as Magento\Catalog\Model\Product, Magento\Catalog\Model\Category, Magento\Customer\Model\Customer, Magento\Quote\Model\Quote, Magento\Sales\Model\Order, and others, we can see that a great deal of these entity types are essentially extending from Magento\Framework\Model\AbstractModel and provide their own values to replace $_eventPrefix = 'core_abstract' and $_eventObject = 'object'. What this means is that we can use events such as $this->_eventPrefix . '_save_commit_after' to specify observers via events.xml.
Let's take a look at the following example, taken from the <MAGENTO_DIR>/module-downloadable/etc/events.xml file:
<config>
<event name="sales_order_save_commit_after">
<observer name="downloadable_observer" instance="Magento\Downloadable\Observer\SetLinkStatusObserver" />
</event>
</config>
Observers are placed inside the <ModuleDir>/Observer directory. Every observer implements a single execute method on the Magento\Framework\Event\ObserverInterface class:
class SetLinkStatusObserver implements \Magento\Framework\Event\ObserverInterface {
public function execute(\Magento\Framework\Event\Observer $observer) {
$order = $observer->getEvent()->getOrder();
}
}
Much like plugins, badly implemented observers can easily cause bugs or even break the entire application. This is why we need to keep our observer small and computationally efficient—to avoid performance bottlenecks.
The cyclical event loop is a trap that's easy to fall into. This happens when an observer, at some point, is dispatching the same event that it listens to. For example, if an observer listens to the model_save_before event, and then tries to save the same entity again within the observer, this would trigger a cyclical event loop.
To make our observers as specific as possible, we need to declare them in an appropriate scope:
- For observing frontend only events, you can declare observers in <ModuleDir>/etc/frontend/events.xml
- For observing backend only events, you can declare observers in <ModuleDir>/etc/adminhtml/events.xml
- For observing global events, you can declare observers in <ModuleDir>/etc/events.xml
Unlike plugins, observers are used for triggering the follow-up functionality, rather than changing the behavior of functions or data which is part of the event they are observing.
Console commands
The built-in bin/magento tool plays a major role – not just in Magento development, but in production deployments as well.
Right out of the box, it provides a dozen commands that we can use to manage caches, indexers, dependency compilation, deploying static view files, creating CSS from LESS, putting our store to maintenance, installing modules, and more.
Quite easily, Magento enables us to add our own commands to its Symfony-like command-line interface (CLI). The Magento CLI essentially extends from Symfony\Component\Console\Command.
The real value in creating our own command lies in the arguments and options that we can make available, thus passing dynamic information to the command.
Magento console commands reside under the <ModuleName>/Console directory, which can further be organized to better accommodate our commands. Magento mostly uses the <ModuleName>/Console/Command directory to place the actual CLI command class, whereas various options and other accompanying classes reside in the <ModuleName>/Console directory.
Conceptually, creating a new CLI command is as easy as doing the following:
- Creating the command class
- Wiring it up via di.xml
- Clearing the cache and compiled directories
Let's create our own simple console command. We will start off by creating the <MAGELICIOUS_DIR>/Core/Console/Command/RunStockImportCommand.php file with the following content:
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RunStockImportCommand extends Command {
const ORDER_ID_ARGUMENT = 'order_id';
const DAYS_BACK_OPTION = 'days_back';
protected function configure() {
$this->setName('magelicious:stock:import')
->setDescription('The Magelicious Stock Import.')
->setDefinition([
new InputArgument(
self::ORDER_ID_ARGUMENT, /* name */
InputArgument::REQUIRED, /* mode REQUIRED or OPTIONAL */
'The argument to set.', /* description */
null /* default */
),
new InputOption(
self::DAYS_BACK_OPTION, /* name */
null, /* shortcut */
InputOption::VALUE_OPTIONAL, /* VALUE_NONE or VALUE_REQUIRED or VALUE_OPTIONAL or VALUE_IS_ARRAY */
'The option to set.' /* description */
)
]);
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output) {
try {
$output->setDecorated(true);
// $input->getArgument(self::ORDER_ID_ARGUMENT);
// $input->getOption(self::DAYS_BACK_OPTION);
// green text
$output->writeln('<info>The info message.</info>');
// yellow text
$output->writeln('<comment>The comment message.</comment>');
// black text on a cyan background
$output->writeln('<question>The question message.</question>');
return \Magento\Framework\Console\Cli::RETURN_SUCCESS;
} catch (\Exception $e) {
// white text on a red background
$output->writeln('<error>' . $e->getMessage() . '</error>');
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
$output->writeln($e->getTraceAsString());
}
return \Magento\Framework\Console\Cli::RETURN_FAILURE;
}
}
}
We then wire it up via <MAGELICIOUS_DIR>/etc/di.xml, as follows:
<type name="Magento\Framework\Console\CommandListInterface">
<arguments>
<argument name="commands" xsi:type="array">
<item name="runStockImport" xsi:type="object">Magelicious\Core\Console\Command\RunStockImportCommand</item>
</argument>
</arguments>
</type>
We can now clear the cache and the compiled directories either by running the php bin/magento cache:clean config followed by php bin/magento setup:di:compile, or by running rm -rf generated/* and rm -rf var/cache/*.
Now, if we run the php bin/magento command, we should see our command on the list:
magelicious
magelicious:stock:import The Magelicious Stock Import.
If we now test our method by running php bin/magento magelicious:stock:import, this should immediately trigger an error, as follows:
[Symfony\Component\Console\Exception\RuntimeException]
Not enough arguments (missing: "order_id").
magelicious:stock:import [--days_back [DAYS_BACK]] [--] <order_id>
Either of the following calls should work:
php bin/magento magelicious:stock:import 000000060
php bin/magento magelicious:stock:import 000000060 --days_back=7
Cron jobs
Creating a new cron job is as easy as doing the following:
- Creating a job definition under the <ModuleName>/etc/crontab.xml file
- Creating a class with a public method that handles the job execution
Let's create a simple cron job. We will start off by creating the <MAGELICIOUS_DIR>/Core/etc/crontab.xml file with the following content:
<group id="default">
<job name="the_job" instance="Magelicious\Core\Cron\TheJob" method="execute">
<schedule>*/15 * * * *</schedule>
</job>
</group>
The instance and method values map to the class and method within that class, which will be executed when cron job is run. The schedule is a cron, like the expression for when the job is to be executed. Unless there are specific requirements telling us otherwise, we can safely use the default group.
We then create the <MAGELICIOUS_DIR>/Core/Cron/TheJob.php file with the following content:
class TheJob {
public function execute() {
// ...
}
}
The Magento console command supports several console commands:
cron
cron:install Generates and installs crontab for current user
cron:remove Removes tasks from crontab
cron:run Runs jobs by schedule
To get our cron job running, we need to make sure that crontab is installed, by running php bin/magento cron:install. This command generates and installs crontab for the current user. We can confirm that by following up with the crontab -e command, like so:
#~ MAGENTO START 6f7c468a10aea2972eab1da53c8d2fce
* * * * * /bin/php /magelicious/bin/magento cron:run 2>&1 | grep -v "Ran jobs by schedule" >> /magelicious/var/log/magento.cron.log
* * * * * /bin/php /magelicious/update/cron.php >> /magelicious/var/log/update.cron.log
* * * * * /bin/php /magelicious/bin/magento setup:cron:run >> /magelicious/var/log/setup.cron.log
#~ MAGENTO END 6f7c468a10aea2972eab1da53c8d2fce
Now, if we execute php bin/magento cron:run, the_job should find its way under the cron_schedule table.
Magento Open Source provides two cron groups: default and index. While the majority of times our cron jobs will be placed under the default group, there might be a need to create a completely new cron group. Luckily, this is quite easy.
To create a new cron group, all we need is a <MAGELICIOUS_DIR>/etc/cron_groups.xml file with the following content:
<config>
<group id="magelicious">
<schedule_generate_every>15</schedule_generate_every>
<schedule_ahead_for>20</schedule_ahead_for>
<schedule_lifetime>15</schedule_lifetime>
<history_cleanup_every>10</history_cleanup_every>
<history_success_lifetime>10080</history_success_lifetime>
<history_failure_lifetime>10080</history_failure_lifetime>
<use_separate_process>0</use_separate_process>
</group>
</config>
While group information is not stored in the cron_schedule table, we can use it via the Magento CLI to run jobs that are specific to a certain group:
php bin/magento cron:run --group=default
Summary
In this chapter, we touched upon some of Magento's keys components. Plugins and event observers provide a powerful way of extending Magento, either by changing the behavior of existing functions or by running some follow-up code in response to certain events.
Moving forward, we will deepen our Magento knowledge further by looking into the install and update scripts, the Entity–Attribute–Value model (EAV), creating new EAV types, indexers, extension, and custom attributes.