If you’re following along over on the Pulse Storm blog, you’ll know we’ve just released Commerce bug 3.1. This is mostly a bug fix/Magento 2.1 compatibility release, but there are a few new features hidden beneath the surface.
A software development tool like Commerce Bug encapsulates a certain way of thinking about, analyzing, and manipulating your code. Magento 2 presents a particular challenge, since no one’s quite sure how to think about Magento 2 yet. Baking a feature into the UI that’s going to prove useless one release cycle from now is a recipe for madness. However, not including a new feature robs Commerce Bug users of something useful.
To split the difference, we’ve started to include our new prototype features as Magento CLI commands. You can see a list of these commands by running the following
$ php bin/magento list ps:cb
//...
Available commands for the "ps:cb" namespace:
ps:cb:scan:context Scans class for DI params repeated in context object.
ps:cb:scan:double-param Scans constructor for double params.
These commands live somewhere between unstable beta and fully polished, ready for a GUI features. They’ll be useful to your development workflows, but may change as new and better best practices develop in the Magento world.
Today we’re going to discuss the first two ps:cb
commands in Commerce bug, both of which offer pre-scanning for Magento 2’s setup:di:compile
command.
Dependency Injection Compilation
To start, we should explain the problem these commands are trying to solve. When you ship a Magento 2 system to production, you need to run the following command
php bin/magento setup:di:compile
This commands scans through the code in your system and pre-generates a number of things (mostly related to the object manager system and dependency injection) that Magento dynamically loads when you’re running in developer mode. This is both a performance and security thing, and discussing it in full is beyond the scope of this article.
What is in the scope of this article is how slow this process can be. While it’s not a “true” compilation in the computer science sense (transforming high level code into assembly code) it can take as long as traditional compile cycles did back in the day. This becomes extra frustrating if your module code fails some of the validation checks setup:di:compile
performs. When this happens, you need to fix your code, and start the compilation process over again for the entire system. There’s no way to compile a single module.
The ps:cb:scan:context
and ps:cb:scan:double-param
commands each check a single PHP file for some of these compilation checks. These commands will let you validate your module’s code before running a full compilation, or let you quickly correct an error that crops up during compilation.
Double Parameter Check
First, let’s talk about the ps:cb:scan:double-param
command. Up until PHP 7 (meaning, for Magento 2 developers, PHP 5.6), PHP was pretty permissive about function parameters. For example, the following code
function example($foo, $foo)
{
echo $foo,"\n";
}
example("Hello World","Goodbye World");
is valid PHP, despite the fact the example
function has two parameters with the same name. PHP just uses the value of the second argument.
When developing Magento 2 extensions, I’ve found myself copy/pasting constructor DI parameters from core classes once I’ve found the object that does what I need. I’ve also found this leads to the occasional parameter with the same name, or injecting the same object twice when parameter lists get long. This usually doesn’t show up as a problem until I run my setup:di:compile
check on PHP 7.
The ps:cb:scan:double-param
command scans a single PHP file for this situation. Scanning a class with a valid constructor
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $resultPageFactory)
{
$this->resultPageFactory = $resultPageFactory;
return parent::__construct($context);
}
looks like this.
$ php bin/magento ps:cb:scan:double-param app/code/Pulsestorm/Nofrillslayout/Controller/Index/Index.php
Param Names is free of dupes.
Param Types is free of dupes.
Scan complete
Whereas scanning a class with an invalid constructor
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $resultPageFactory
)
{
$this->resultPageFactory = $resultPageFactory;
return parent::__construct($context);
}
looks like this.
$ php bin/magento ps:cb:scan:double-param app/code/Pulsestorm/Nofrillslayout/Controller/Index/Index.php
Param Names has dupes: resultPageFactory
Param Types has dupes: Magento\Framework\View\Result\PageFactory
Scan complete
The command reports on both the repeat of a type hint as well as the parameter name.
Context Object Repeat
Another common problem when running setup:di:compile
is accidentally duplicating an injected parameter that’s already available in the context object.
If you know all those words, but are still confused, don’t worry, we’ll explain.
If you’ve worked your way through the object manager series, you know that Magento has a dependency injection system that automatically creates objects for you in object constructors.
With the following constructor
public function __construct(
\Magento\Framework\View\Result\PageFactory $resultPageFactory
)
{
$this->resultPageFactory = $resultPageFactory;
}
Magento 2 will automatically instantiate a Magento\Framework\View\Result\PageFactory
object, and pass it in as the $resultPageFactory
parameter.
This dependency injection system is designed to make external object dependencies explicit in a class, make the classes easier to test, and help discourage too many dependencies in a class.
Regarding that last item — while Magento Inc. spent 4 years working on Magento 2, they spent far more time building new systems than they did cleaning up old ones. Many Magento object types have a large number of dependencies, and rather than refactor these out, Magento Inc. simply carried them forward.
For example, a Magento 2 constructor class has eleven dependencies
public function __construct(
\Magento\Framework\View\Result\PageFactory $resultPageFactory
)
{
\Magento\Framework\App\RequestInterface $request,
\Magento\Framework\App\ResponseInterface $response,
\Magento\Framework\ObjectManagerInterface $objectManager,
\Magento\Framework\Event\ManagerInterface $eventManager,
\Magento\Framework\UrlInterface $url,
\Magento\Framework\App\Response\RedirectInterface $redirect,
\Magento\Framework\App\ActionFlag $actionFlag,
\Magento\Framework\App\ViewInterface $view,
\Magento\Framework\Message\ManagerInterface $messageManager,
\Magento\Framework\Controller\Result\RedirectFactory $resultRedirectFactory,
\Magento\Framework\Controller\ResultFactory $resultFactory
}
These are all the objects a Magento 2 controller needs in order to do its job.
This many classes/objects creates a problem. If end-user-programmers wants to create their own controllers with an injected dependency, this means they’d need to
- Copy all these dependencies from the base class
- Make a call to
parent::__construct
with the parameters in the correct order - Keep all those
parent::
calls up-to-date when the base class changes in future versions of Magento
This is the problem Context objects solve. If you look at the actual __construct
method for a base action controller in Magento 2
#File: vendor/magento/framework/App/Action/Action.php
public function __construct(
\Magento\Framework\App\Action\Context $context
)
{
parent::__construct($context);
$this->_objectManager = $context->getObjectManager();
$this->_eventManager = $context->getEventManager();
$this->_url = $context->getUrl();
$this->_actionFlag = $context->getActionFlag();
$this->_redirect = $context->getRedirect();
$this->_view = $context->getView();
$this->messageManager = $context->getMessageManager();
}
You’ll see only a single injected parameter — Magento\Framework\App\Action\Context
. However, if you look at the source of this context object
#File: vendor/magento/framework/App/Action/Context.php
public function __construct(
\Magento\Framework\App\RequestInterface $request,
\Magento\Framework\App\ResponseInterface $response,
\Magento\Framework\ObjectManagerInterface $objectManager,
\Magento\Framework\Event\ManagerInterface $eventManager,
\Magento\Framework\UrlInterface $url,
\Magento\Framework\App\Response\RedirectInterface $redirect,
\Magento\Framework\App\ActionFlag $actionFlag,
\Magento\Framework\App\ViewInterface $view,
\Magento\Framework\Message\ManagerInterface $messageManager,
\Magento\Framework\Controller\Result\RedirectFactory $resultRedirectFactory,
ResultFactory $resultFactory
) {
}
you’ll see all the needed dependencies. The context objects allow Magento core developers to hide the dependency burden of Magento’s core classes behind a single class. As of Magento 2.1, there are twenty one different context object types
vendor/magento/framework/App/Action/Context.php
vendor/magento/framework/App/Helper/Context.php
vendor/magento/framework/Code/Test/Unit/Validator/_files/ClassesForArgumentSequence.php
vendor/magento/framework/Code/Test/Unit/Validator/_files/ClassesForConstructorIntegrity.php
vendor/magento/framework/Code/Test/Unit/Validator/_files/ClassesForContextAggregation.php
vendor/magento/framework/Model/Context.php
vendor/magento/framework/Model/ResourceModel/Db/Context.php
vendor/magento/framework/Module/Setup/Context.php
vendor/magento/framework/View/Asset/File/Context.php
vendor/magento/framework/View/Element/Context.php
vendor/magento/framework/View/Element/UiComponent/Context.php
vendor/magento/magento2-base/setup/src/Magento/Setup/Model/ModuleContext.php
vendor/magento/module-authorization/Model/CompositeUserContext.php
vendor/magento/module-catalog/Model/Layer/Context.php
vendor/magento/module-customer/Model/Authorization/CustomerSessionUserContext.php
vendor/magento/module-eav/Model/Entity/Context.php
vendor/magento/module-rule/Model/Condition/Context.php
vendor/magento/module-user/Model/Authorization/AdminSessionUserContext.php
vendor/magento/module-webapi/Model/Authorization/GuestUserContext.php
vendor/magento/module-webapi/Model/Authorization/OauthUserContext.php
vendor/magento/module-webapi/Model/Authorization/TokenUserContext.php
Context and Redundancy
One side effect of all this can be redundancy. If I’m an end-user-programmer creating my own controller class and I need to generate a URL, I may try injecting a Magento\Framework\UrlInterface
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\UrlInterface $urlMaker,
)
{
$this->urlMaker = $urlMaker;
return parent::__construct($context);
}
even though the context object already has a URL object inside it. Magento, by itself, will let you do this.
However, the setup:di:compile
command checks for these sorts of redundancies. If it detects one, compilation is halted. This is the situation ps:cb:scan:context
scans for.
A scan of a class with the above, invalid constructor, would look like this.
$ php bin/magento ps:cb:scan:context app/code/Pulsestorm/Nofrillslayout/Controller/Index/Index.php
Found param in original object that's in context object.
- Magento\Framework\UrlInterface
Original Object: /Users/alanstorm/Sites/magento-2-1-0.dev/project-community-edition/app/code/Pulsestorm/Nofrillslayout/Controller/Index/Index.php
Context Object: /Users/alanstorm/Sites/magento-2-1-0.dev/project-community-edition/vendor/magento/framework/App/Action/Context.php
Wrap Up
While far from complete, these two commands should dramatically shorten the release cycle of anyone working on Magento 2 modules meant for production deployment. If there’s additional setup:di:compile
code validation that’s tripping you up, please let me know and we’ll get them added to the new feature tracker.