Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Magento 2 Development Quick Start Guide
Magento 2 Development Quick Start Guide

Magento 2 Development Quick Start Guide: Build better stores by extending Magento

eBook
€11.99 €17.99
Paperback
€21.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
OR
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Table of content icon View table of contents Preview book icon Preview Book

Magento 2 Development Quick Start Guide

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)
AMPPS is an easy to use, all in one LAMP/MAMP/WAMP software stack from Softaculous, which enables Apache, MySQL, and PHP. With AMPPS, you can even install Magento 2.x by the click of a button, which means it comes loaded with all the right PHP extensions. While it isn't suited for production purposes, it comes in handy for quickly kicking the development environment. See http://www.ampps.com/ for more information. Consult the devdocs (https://devdocs.magento.com) for Magento technology stack requirements.

Technical requirements

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:

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 \
[email protected] \
--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.

Magento automatically assigns an admin URL during console installation, unless explicitly specified through the install command via the --backend-frontname option.
Out of all the installation options listed, only the following are actually required: --admin-firstname, --admin-lastname, --admin-email, --admin-user, and --admin-password. It is worth taking some time to read through the official Magento documentation (https://devdocs.magento.com) and looking at what the rest of the installation options have to offer.

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
Carefully balancing developer mode with some of the cache types being enabled/disabled can provide optimal performance during development.

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:

  1. Inject the new item under the routerList argument of the Magento\Framework\App\RouterList type via the di.xml file.
  2. Create a router file (by using the match method, which implements \Magento\Framework\App\RouterInterface).
  3. 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:

  1. Register a route via router.xml.
  2. Create an abstract controller file (as an abstract class, which extends \Magento\Framework\App\Action\Action).
  1. Create an action controller file (which extends the main controller file with the execute method, and implements \Magento\Framework\App\ActionInterface).
  2. Return an instance of \Magento\Framework\Controller\ResultInterface.
The separation of the controller into the main and action controller files is not a technical requirement, but rather a recommended organizational one. Magento does this across the majority of its modules.

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;
A quick lookup for the $this->resultFactory-> create string, across the entire <MAGENTO_DIR> directory, can give us lots of examples of how to use the ResultFactory for our own code.

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.

When developer mode is enabled, we can manually clean the cache, compilation, and static files via the rm -rf var/cache/* && rm -rf var/page_cache/* && rm -rf var/view_preprocessed/* && rm -rf generated/* && rm -rf pub/static/* command. Under limited use cases, this can provide a faster development workflow.

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.

Creating the <VendorName>/Core module is often a good practice when working on a project with lots of custom <VendorName> modules. Used carefully, the Core module can provide common bits that are shared across several other modules. The tab element of the system.xml file, which is used to provide a sidebar menu presence under Magento's admin Stores | Settings | Configuration, is a nice example. Similarly, we can use it to provide top-level access resources for other modules to use.

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.

Disabled cache types are not cleaned. Use the cache:flush command with care, as flushing the cache type purges the entire cache storage. This, in turn, might affect other applications that are using the same storage.

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);
}
}
The direct use of the objectManager is highly discouraged, as it prevents type validation and type hinting that a factory class provides.

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 ([email protected] 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
See <MAGENTO_DIR>/framework/Data/etc/argument/types.xsd and <MAGENTO_DIR>/framework/ObjectManager/etc/config.xsd for specific type definitions.

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.

Most of the virtualType name attributes across Magento take the form of a non-existing fully qualified class name, though this can be any character combination that's allowed in PHP array keys.

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.

While in developer mode, Magento performs automatic compilation, meaning that changes to di.xml are automatically picked up. Sometimes, however, if we stumble upon unexpected results, running the bin/magento setup:di:compile console command or even manually clearing the generated folder (rm -rf generated/*) might help sort out the issues.

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:

  1. The before keyword is appended to the observed instance method. If the observed method is called getSomeValue, then the plugin method is called beforeGetSomeValue.
  2. 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.
  3. All other parameters of the plugin method must match the parameters of the observed method.
  4. 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:

  1. The first parameter coming into the plugin is the observed type instance.
  2. 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.
  3. All other parameters are parameters of the original observed method.
  4. 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:

  1. The first parameter coming into the plugin is an observed type instance.
  2. 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).
  3. All other parameters are parameters of the observed method.
  4. 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:

  1. Creating the command class
  2. Wiring it up via di.xml
  3. 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:

  1. Creating a job definition under the <ModuleName>/etc/crontab.xml file
  2. 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.

Depending on the schedule_generate_every and schedule_ahead_for options for a particular cron group, we might not see some cron jobs instantly showing up in 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.

Left arrow icon Right arrow icon

Key benefits

  • A straightforward guide to developing with Magento
  • Examples of different types of extension
  • Customize the Magento storefront and admin areas

Description

Magento is an open-source, enterprise-level e-commerce platform with unlimited scope for customization. This makes it a great choice not only for vendors but for developers as well. This book guides you through Magento development, teaching you how to develop modules that extend or change its functionality, leading to more ?exible and profitable Magento stores. You start with a structural overview of the key Magento development components. You will learn where things such as plugins, events, models, controllers, layouts, and UI components ft into the development landscape. You will go through examples of using these components to extend Magento. As you progress, you will be building a diverse series of small but practical Magento modules. By the end of this book, you will not only have a solid foundation in the Magento development architecture; but you will also have practical experience in developing modules to customize and extend Magento stores.

Who is this book for?

This book is for competent PHP developers, with only basic knowledge of the Magento platform required.

What you will learn

  • Develop a simple shipping module
  • Build admin interfaces with the built-in form and listing UI components
  • Implement JavaScript components for improved customer experience
  • Accommodate vendor needs by adding new catalog-related features
  • Develop your way to a better checkout
  • Improve customer interaction with new customer-related extensions
  • Create new web APIs to make your modules more extensible
Estimated delivery fee Deliver to Belgium

Premium delivery 7 - 10 business days

€17.95
(Includes tracking information)

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Sep 21, 2018
Length: 218 pages
Edition : 1st
Language : English
ISBN-13 : 9781789343441
Vendor :
Magento
Languages :
Concepts :
Tools :

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
OR
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Estimated delivery fee Deliver to Belgium

Premium delivery 7 - 10 business days

€17.95
(Includes tracking information)

Product Details

Publication date : Sep 21, 2018
Length: 218 pages
Edition : 1st
Language : English
ISBN-13 : 9781789343441
Vendor :
Magento
Languages :
Concepts :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 88.97
Magento 2 Theme Design
€29.99
Magento 2 Beginners Guide
€36.99
Magento 2 Development Quick Start Guide
€21.99
Total 88.97 Stars icon
Banner background image

Table of Contents

10 Chapters
Understanding the Magento Architecture Chevron down icon Chevron up icon
Working with Entities Chevron down icon Chevron up icon
Understanding Web APIs Chevron down icon Chevron up icon
Building and Distributing Extensions Chevron down icon Chevron up icon
Developing for Admin Chevron down icon Chevron up icon
Developing for Storefront Chevron down icon Chevron up icon
Customizing Catalog Behavior Chevron down icon Chevron up icon
Customizing Checkout Experiences Chevron down icon Chevron up icon
Customizing Customer Interactions Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Rating distribution
Full star icon Full star icon Full star icon Full star icon Full star icon 5
(2 Ratings)
5 star 100%
4 star 0%
3 star 0%
2 star 0%
1 star 0%
cru Dec 27, 2019
Full star icon Full star icon Full star icon Full star icon Full star icon 5
da leggere con attenzione
Amazon Verified review Amazon
Danny Z Jan 26, 2021
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Good stuff, relevant even until this year (2021 as of this writing) I like the layout of the chapters and progression as you read the book front-to-back. Picked up the other author's book on Magento 2 since I enjoyed this one so much.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact [email protected] with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at [email protected] using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on [email protected] with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on [email protected] within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on [email protected] who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on [email protected] within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela