Book Image

Magento 2 Development Quick Start Guide

By : Branko Ajzele
Book Image

Magento 2 Development Quick Start Guide

By: Branko Ajzele

Overview of this book

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.
Table of Contents (11 chapters)

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.