A little about AOP
Aspect Oriented Programming (AOP) is a programming paradigm that tries to increase modularity of code. It does this by allowing the separation of “cross-cutting” concerns.
This means that the code is streamlined to do exactly what it needs to do, without having to worry about code that operates to undertake processing throughout the rest of the application.
An example
Let’s have a look at a quick example. This example focuses on the need to log within an application.
1 2 3 4 5 6 7 8 9 |
public function doSomething() { try { //do something $this->logger->debug("something"); } catch (\Exception $e) { $this->logger->critical($e); } } |
As you can see from the example, we have a large amount of code that is taking care of logging, and logging any exceptions, but the main code is relatively small. If we wanted to swap out our logging pattern, it could be quite a large undertaking, as you can imagine that this kind of code would be littered everywhere within our application.
Wouldn’t it be much nicer, if our code just did what it needed to do.
1 2 3 4 |
public function doSomething() { //do something } |
How does AOP work?
AOP tries to overcome this by adding additional behaviour to our existing code – an “advice” – without modifying the code itself. Instead, it specifies which code is modified via a “point cut” specification.
AOP allows developers to apply these “advice” to crosscutting concerns – logic applicable throughout the application, that affects the entire application – such as logging in our example above. Security and data transfer are also concerns which are needed in almost every module of an application.
A segway into SOLID
Let’s have a quick segway into SOLID principles. These are five core principles of object oriented programming and design. The concept behind these principles is that it will allow for greater maintainable code that can be extended over time.
The principles are:
- Single responsibility
- Open-closed
- Liskov substitution
- Interface segregation
- Dependency inversion
What we’re looking at is the Single responsibility. We want the code to take care of it’s concerns/responsibilities only. Classes and methods should only have one responsibility, and do one thing.
AOP allows us to satisfy this requirement. Our core classes and methods do the task that we expect, and the cross cutting concerns are taken care of within code that interacts with our existing code.
Plugins in Magento2
Magento2’s implementation of AOP is done in the form of Plugins – you may also see these referred to as interceptors in Magento documentation.
Plugins allow a developer to “Listen” to any public method call made on an object manager controlled object and take programmatic action. It allows for the behaviour of the method to be modified and have code run before, after, or around the method call.
This means you could extend, or even substitute an existing method.
Additionally, you can change the return value of any method call made on an object manager controlled object, change the arguments of any method call made to an object manager controlled object, all whilst other plugins are doing the same thing to the same method in a predictable way.
Magento implements this using the interceptor pattern (why you might see these referred to as interceptors).
As mentioned in the wikipedia article referenced above, one of the key aspects of the interceptor pattern is that the rest of the system does not have to know something has been added or changed and can keep working as before.
Limitations to the implementation
Magento’s implementation means that you cannot use AOP with any of the following:
- Objects that are instantiated before Magento\Framework\Interception is bootstrapped
- Final methods
- Final classes
- Any class that contains at least one final public method
- Non-public methods
- Class methods (such as static methods)
- __construct
- Virtual types
Our AOP options
Before: Executes before a join point, but which does not have the ability to prevent execution flow proceeding to the join point (unless it throws an exception).
In Magento2, we use the method prefix before
and simply camel case the method name.
After: Executed after a join point completes normally: for example, if a method returns without throwing an exception.
In Magento2, we use the method prefix after
and simply camel case the method name.
Around: Surrounds a join point such as a method invocation.
Around can perform custom behaviour before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.
In Magento2, we use the method prefix around
and simply camel case the method name.
Defining Plugins in Magento2
Magento2 allows developers to create plugins using standard PHP classes, and reference them using XML within di.xml
.
There are a series of Required options:
- Type name: The name of a class or interface that needs to be followed.
- Plugin name: The name for the new plugin in Magento 2. This is arbitrary and is simply used during mergin XML to give your plugin a unique reference.
- Plugin type: The name of a plugin’s class or its virtual type.
\Vendor\Module\Plugin\ModelName\Plugin
.
There are additionally couple of optional options:
- Plugin sortOrder: Set the order in which the plugin should be called.
- Plugin disabled: That allows you enable or disable a plugin quickly. As the default configuration, the chosen value is false. This allows you to write a global
di.xml
and then override (and disable) a Plugin in an area specificdi.xml
such as frontend / adminhtml.
Some examples from core
Before
Here we have an example from \Magento\Theme\Model\Theme\Plugin\Registration
.
Note the keyword before
in front of the method we want to intercept dispatch
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/** * Add new theme from filesystem and update existing * * @param AbstractAction $subject * @param RequestInterface $request * * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeDispatch( AbstractAction $subject, RequestInterface $request ) { try { if ($this->appState->getMode() != AppState::MODE_PRODUCTION) { $this->themeRegistration->register(); $this->updateThemeData(); } } catch (LocalizedException $e) { $this->logger->critical($e); } } |
It’s achieved with the following entry in di.xml
specifically in the adminhtml scope.
1 2 3 |
<type name="Magento\Backend\App\AbstractAction"> <plugin name="themeRegistrationFromFilesystem" type="Magento\Theme\Model\Theme\Plugin\Registration"></plugin> </type> |
After
This function specifically invalidates the indexer registry when an item is deleted.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * Invalidate design config grid indexer on store removal * * @param StoreStore $subject * @param StoreStore $result * @return StoreStore * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterDelete(StoreStore $subject, $result) { $this->indexerRegistry->get(Config::DESIGN_CONFIG_GRID_INDEXER_ID)->invalidate(); return $result; } |
It’s achieved with the following entry in di.xml
.
1 2 3 |
<type name="Magento\Store\Model\Group"> <plugin name="themeDesignConfigGridIndexerStoreGroup" type="Magento\Theme\Model\Indexer\Design\Config\Plugin\StoreGroup"></plugin> </type> |
Around
Here we have an example from \Magento\Theme\Model\Url\Plugin\Signature
.
This functionality ensures that any call to getBaseUrl()
returns a string that ends with a forward slash. Note the keyword around
in front of the method we want to intercept getBaseUrl
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** * Append signature to rendered base URL for static view files * * @param \Magento\Framework\Url\ScopeInterface $subject * @param callable $proceed * @param string $type * @param null $secure * @return string * @see \Magento\Framework\Url\ScopeInterface::getBaseUrl() * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function aroundGetBaseUrl( \Magento\Framework\Url\ScopeInterface $subject, \Closure $proceed, $type = \Magento\Framework\UrlInterface::URL_TYPE_LINK, $secure = null ) { $baseUrl = $proceed($type, $secure); if ($type == \Magento\Framework\UrlInterface::URL_TYPE_STATIC && $this->isUrlSignatureEnabled()) { $baseUrl .= $this->renderUrlSignature() . '/'; } return $baseUrl; } |
It’s achieved with the following entry in di.xml
1 2 3 |
<type name="Magento\Framework\Url\ScopeInterface"> <plugin name="urlSignature" type="Magento\Theme\Model\Url\Plugin\Signature"></plugin> </type> |
Using around
You’ll notice in the method above that there is a call to $proceed
, a PHP closure that allows the method to call the method it is intercepting. With great power, comes great responsibility. It is the around method’s responsibility to call the callable. If it doesn’t, it prevents the execution of all the plugins that follow it.
When you use around on a method that accepts arguments, your plugin method must also accept those arguments – and you must forward them when you invoke the proceed callable. You must match the original signature of the method, including default parameters and type hints – otherwise, your plugin may introduce unintended exceptions.
If you’re doing nothing with these arguments, you can use variadics and argument unpacking to achieve this.
This doesn’t seem to be used in CORE at the moment, but allows you to do the following:
1 |
public function aroundSave(\X\Y\Model\Z $subject, callable $proceed, ...$args) |
Prioritisation
I’d suggest checking out the dev docs for more details on prioritisation, as they’re got some great examples: Prioritizing plugins.
Image Credit: Push buttons and extra gauges
Pingback: How Magento2 implements Plugins - Freelance Magento, PHP & WordPress Development, based in York, UK