Categories


Archives


Recent Posts


Categories


Magento 2: DI Compile Pre-Scan with Commerce Bug

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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

  1. Copy all these dependencies from the base class
  2. Make a call to parent::__construct with the parameters in the correct order
  3. 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.

Originally published August 22, 2016