I recently wrote a post about Aspect Oriented Programming, and how developers can use it within Magento 2. However, I didn’t cover anything about what Magento 2 is actually doing in the background. How exactly is it providing the Plugin?
Interceptors are how Magento 2’s object system implements the plugin system.
In theory, you don’t need to worry about how Magento provides the underlying plugin system in order to use it.
When you ask Magento for an object, it may return an interceptor, or it might return your referenced class.
How it’s implemented
If a class has a plugin configured, the object manager will return an interceptor.
Magento handles the delivery of an interceptor different ways depending on how your environment is configured.
Production Mode
In production mode, Magento will look through your solution during compilation, and create interceptors for plugins where required.
Magento handles plugin creation with a separate PHP autoloader. If a developer triggers a PHP autoload event, and the composer based autoloader can’t find the class file, a second registered autoloader is called:
Magento\Framework\Code\Generator\Autoloader
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * Load specified class name and generate it if necessary * * @param string $className * @return bool True if class was loaded */ public function load($className) { if (!class_exists($className)) { return Generator::GENERATION_ERROR != $this->_generator->generateClass($className); } return true; } |
So, what’s happening here?
Magento is making a call to Magento\Framework\Code\Generator::generateClass
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/** * Generate Class * * @param string $className * @return string | void * @throws \RuntimeException * @throws \InvalidArgumentException */ public function generateClass($className) { $resultEntityType = null; $sourceClassName = null; foreach ($this->_generatedEntities as $entityType => $generatorClass) { $entitySuffix = ucfirst($entityType); // If $className string ends with $entitySuffix substring if (strrpos($className, $entitySuffix) === strlen($className) - strlen($entitySuffix)) { $resultEntityType = $entityType; $sourceClassName = rtrim( substr($className, 0, -1 * strlen($entitySuffix)), '\\' ); break; } } if ($skipReason = $this->shouldSkipGeneration($resultEntityType, $sourceClassName, $className)) { return $skipReason; } $generatorClass = $this->_generatedEntities[$resultEntityType]; /** @var EntityAbstract $generator */ $generator = $this->createGeneratorInstance($generatorClass, $sourceClassName, $className); if ($generator !== null) { $this->tryToLoadSourceClass($className, $generator); if (!($file = $generator->generate())) { $errors = $generator->getErrors(); throw new \RuntimeException(implode(' ', $errors)); } if (!$this->definedClasses->isClassLoadableFromMemory($className)) { $this->_ioObject->includeFile($file); } return self::GENERATION_SUCCESS; } } |
As you can see from the code above, after name matching, it checks to see if there is any reason that the generation should be skipped.
Let’s take a look at an example.
We’re going to take a look at vendor/magento/module-customer/Model/Plugin/AllowedCountries.php
. As you can see, we have a before method called on getAllowedCountries
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
namespace Magento\Customer\Model\Plugin; use Magento\Customer\Model\Config\Share; use Magento\Store\Api\Data\WebsiteInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; /** * Class AllowedCountries */ class AllowedCountries { /** * @var \Magento\Customer\Model\Config\Share */ private $shareConfig; /** * @var StoreManagerInterface */ private $storeManager; /** * @param Share $share * @param StoreManagerInterface $storeManager */ public function __construct( Share $share, StoreManagerInterface $storeManager ) { $this->shareConfig = $share; $this->storeManager = $storeManager; } /** * Retrieve all allowed countries or specific by scope depends on customer share setting * * @param \Magento\Directory\Model\AllowedCountries $subject * @param string | null $filter * @param string $scope * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeGetAllowedCountries( \Magento\Directory\Model\AllowedCountries $subject, $scope = ScopeInterface::SCOPE_WEBSITE, $scopeCode = null ) { if ($this->shareConfig->isGlobalScope()) { //Check if we have shared accounts - than merge all website allowed countries $scopeCode = array_map(function (WebsiteInterface $website) { return $website->getId(); }, $this->storeManager->getWebsites()); $scope = ScopeInterface::SCOPE_WEBSITES; } return [$scope, $scopeCode]; } } |
Magento creates an interceptor, thus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
namespace Magento\Directory\Model\AllowedCountries; /** * Interceptor class for @see \Magento\Directory\Model\AllowedCountries */ class Interceptor extends \Magento\Directory\Model\AllowedCountries implements \Magento\Framework\Interception\InterceptorInterface { use \Magento\Framework\Interception\Interceptor; public function __construct(\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Store\Model\StoreManagerInterface $storeManager) { $this->___init(); parent::__construct($scopeConfig, $storeManager); } /** * {@inheritdoc} */ public function getAllowedCountries($scope = 'website', $scopeCode = null) { $pluginInfo = $this->pluginList->getNext($this->subjectType, 'getAllowedCountries'); if (!$pluginInfo) { return parent::getAllowedCountries($scope, $scopeCode); } else { return $this->___callPlugins('getAllowedCountries', func_get_args(), $pluginInfo); } } |
Magento is extending the original class to allow our additional code to run. It returns the Interceptor class rather than the original.
This explains a few of the limitations listed in the previous article, such as why you cannot use a Plugin on a class with a final method, or on static method calls.
Developer Mode
In developer mode, where everything is done on the fly, the following code is run:
1 2 3 4 5 6 7 8 9 10 |
public function getInstanceType($instanceName) { $type = parent::getInstanceType($instanceName); if ($this->interceptionConfig && $this->interceptionConfig->hasPlugins($instanceName) && $this->interceptableValidator->validate($instanceName) ) { return $type . '\\Interceptor'; } return $type; } |
From vendor/magento/framework/Interception/ObjectManager/Config/Developer.php
Magento internally uses the vendor/magento/framework/Interception/ObjectManager/InterceptableValidator
class in order to validate an interceptor.
Image Credit: Ferrari 250 Inter
Comment or tweet @douglasradburn