If you’re new to PHP, you may find the concept of autoloading a little confusing. When PHP first introduced classes, there was no defined way to load class definition files — end-user-programmers were expected to use require and include to pull in the classes definitions themselves. The require
and include
statements, while useful, are “dumb” includes. When you use them, PHP treats the code in the included/required file as though you’re copy/pasting it directly into your PHP file. (While the introduction of namespaces in PHP 5.3+ has given include
and require
a bit of intelligence, autoloading pre-dates the namespace feature.)
When PHP set out to solve this problem, rather than create specific rules for loading class files, they created the autoloader system. Autoloading lets end-user-programmers define a function that decides how a class file should be loaded. This shielded the PHP core team from doing something specific and unpopular, and shifted ultimate responsibility into the hands of the programmers writing PHP applications.
This, in turn, led the myriad new PHP frameworks of the time (Code Igniter, Cake, Zend, etc.) to develop autoloaders for their frameworks, freeing the PHP application developer from writing their own autoloader. Instead, a developer simply needs to conform to the rules of the framework when creating their class names and files.
This, somewhat predictably, led to competing and conflicting standards on “the right” way to create a PHP autoloader. It was often hard to use code from competing libraries together. In recent years, the combination of PHP-FIG’s PSR-0 and PSR-4 autoloading standards and the adoption of those standards in PHP Composer means the average PHP developer shouldn’t need to worry about autoloading at all. If you’re managing your project via composer packages, and those packages conform to one of the PSR standards (or otherwise use composer’s flexible autoloading features), you can usually start instantiating classes and be assured that, behind the scenes, PHP will load the definition files automatically.
That said, while many frameworks have adopted the PSR/composer standards for new code going forward, there are still many instances where a framework’s proprietary autoloader will be used. In addition, PHP’s ability to “hook into” a class’s initial instantiation is a tempting target for so called “meta-programming”. While a junior developer can usually live an autoloader free life-style, if you’re going to spend more than a few years in the PHP trenches, learning how to diagnose a system’s autoloaders is often an important debugging and performance tuning skill.
Today we’re going to examine Magento 2’s use of PHP autoloaders, and the code generation these autoloaders enable. While the specifics of this article refer to Magento 2.1.0, the concepts should apply across versions.
Finding the Autoloaders
First, we’ll want to take a look at what sort of autoloaders Magento has registered. We can use PHP’s spl_autoload_functions
function to return an array of registered PHP callbacks. If you’re not familiar with them, callbacks, (or callables), are a special PHP pseudo-type that pre-date PHP having first class anonymous functions.
While we won’t cover callables in full today (read the manual), a callback is basically a string, or a two element array that indicates a PHP function or method to be called. Despite all still-supported versions of PHP having first class anonymous functions (i.e. “closures”), callbacks remain popular because they’re often easier to identify and debug.
Getting back to our problem, let’s add the following debugging code to the end of Magento’s index.php
file. (Make sure you’re editing the right index.php
— Magento has two!)
#File: index.php
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
/** @var \Magento\Framework\App\Http $app */
$app = $bootstrap->createApplication('Magento\Framework\App\Http');
$bootstrap->run($app);
//our debugging code below
foreach(spl_autoload_functions() as $callback)
{
if(is_string($callback))
{
var_dump($callback);
}
if(is_object($callback))
{
var_dump(get_class($callback));
}
if(is_array($callback))
{
var_dump(
get_class($callback[0]) . '::' . (string) $callback[1]
);
}
}
Without getting too deeply into the details, this snippet of debugging code will run through all the registered autoloader callbacks, and dump out a string that will let us find their definitions. If you load any page in Magento with the above in place, you should see the following.
string 'Composer\Autoload\ClassLoader::loadClass' (length=40)
string 'Magento\Framework\Code\Generator\Autoloader::load' (length=49)
If you see additional entries, it means one of your Magento extensions or composer packages has registered an additional autoloader.
Magento 2 and Composer
These two strings are mostly good news. The first, Composer\Autoload\ClassLoader::loadClass
, indicates the loadClass
method in the Composer\Autoload\ClassLoader
class file. This is composer’s stock autoloader. If you want to learn about composer’s autoloader in detail, the Laravel, Composer, and the State of Autoloading series is a good place to start. The short version is composer builds an autoloader based on each included package’s composer.json file, and caches path information in the following files.
vendor/composer/autoload_classmap.php
vendor/composer/autoload_files.php
vendor/composer/autoload_namespaces.php
vendor/composer/autoload_psr4.php
vendor/composer/autoload_real.php
vendor/composer/autoload_static.php
These files are generated whenever you run composer’s install
, update
, or dumpautoload
command. Most (all?) Magento core modules register a PSR-4 autoloader, which means you’ll find the cached autoloading information in vendor/composer/autoload_psr4.php
.
So, if Magento 2 hews closely to the PSR-4 autoloading standard — what’s the Magento\Framework\Code\Generator\Autoloader::load
callback for? Before we can talk about that, we need to talk about Magento 2, code generation, and design patterns.
Magento 2 Code Generation
Magento 2 applies (or, depending on who you ask, mis-applies) traditional “classical OOP” design patterns in their systems code. Whatever you think about applying patterns, patterns originally created to give C++ programmers access to dynamic like feature, in a loosely typed language like PHP Whatever you think about the design patterns approach to programming, it’s hard to argue that they don’t lead to a lot of verbose, boilerplate code.
In order to combat this verboseness, Magento 2 has a dynamic code generation system in place. When you need to implement a certain boilerplate class (Proxies, Interceptors, Factories, etc), all you need to do is name your class in a certain way, use the class name in the object manager or a DI constructor, and Magento will automatically generated the boilerplate for you in the var/generation
folder. In production systems, you pre-generate every one of these classes by running the setup:di:compile
command.
While this automatic code generation is not traditionally part of classical OOP, it does (after some initial confusion) ease the burden for developers working in the system.
Class Generation and Autoloading
After reading the above, the more curious among you may be wondering
Wait, how does that code generation work? PHP doesn’t have features like that!
This brings us back to the second autoloader
string 'Magento\Framework\Code\Generator\Autoloader::load' (length=49)
Magento’s core developers have cleverly hijacked PHP’s autoloading system to create an automatic code generation system. The load
method on the Magento\Framework\Code\Generator\Autoloader
class will
- Scan the class name for certain appended strings
- If those strings match a special list (
Proxy
,Factory
, etc. — see below), and the class does not already exist on disk and/or in memory, instantiate a specific generator class - Use that generator class to generate a boilerplate class
- Automatically include the just generated class definition
If that didn’t quite make sense, don’t worry. The rest of this article is going to dive into the entire generation process in fine detail.
Tracing an Interceptor
If you’ve worked your way through the object manager series, you know that Interceptor
classes are an important part of Magento 2’s plugin system. If you’ve configured a plugin for the class Pulsestorm\TutorialPlugin\Model\Example
, Magento will attempt to instantiate a Pulsestorm\TutorialPlugin\Model\Example\Interceptor
class.
The first time this happens, the standard composer autoloaders won’t find the interceptor class. This means PHP invokes the second aforementioned autoloader method.
#File: vendor/magento/framework/Code/Generator/Autoloader.php
public function load($className)
{
if (!class_exists($className)) {
return Generator::GENERATION_ERROR != $this->_generator->generateClass($className);
}
return true;
}
The $className
variable will contain a string of the class we’re trying to instantiate. So, with some imaginary x-ray code specs on, the above actually looks like
#File: vendor/magento/framework/Code/Generator/Autoloader.php
public function load($className='Pulsestorm\TutorialPlugin\Model\Example\Interceptor')
{
if (!class_exists('Pulsestorm\TutorialPlugin\Model\Example\Interceptor')) {
return Generator::GENERATION_ERROR != $this->_generator->generateClass('Pulsestorm\TutorialPlugin\Model\Example\Interceptor');
}
return true;
}
This call to the autoloader wraps a call to the following method
#File: vendor/magento/framework/Code/Generator/Autoloader.php
$this->_generator->generateClass(...)
We’ll need to find out what sort of object is in the _generator
property. If we look at the autoloader’s constructor,
#File: vendor/magento/framework/Code/Generator/Autoloader.php
public function __construct(
\Magento\Framework\Code\Generator $generator
) {
$this->_generator = $generator;
}
We can see the _generator
property is an instance of the Magento\Framework\Code\Generator
class. As an aside, this pattern of seeing a property, jumping to the constructor, and checking the automatic constructor dependency injection type hint is the majority of Magento 2 debugging work.
If we take a look at the Magento\Framework\Code\Generator::generateClass
method’s definition, we’ll see the following.
#File: vendor/magento/framework/Code/Generator.php
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;
}
}
Speaking in broad terms, this code
- Checks if the class name is one Magento needs to generate
- Checks if there’s a reason we shouldn’t generate the class
- Generates the class
So, despite the amount of code involved, the job this method has is relatively simple. Breaking it down further, in the following code
$resultEntityType = null;
$sourceClassName = null;
foreach ($this->_generatedEntities as $entityType => $generatorClass) {
//...
}
Magento defines two variables outside of the foreach
loop. It’s the foreach
loop’s job to define each of these variables. The $sourceClassName
is the class that our generated class is based on. i.e., for our Pulsestorm\TutorialPlugin\Model\Example\Interceptor
class, the source class will be Pulsestorm\TutorialPlugin\Model\Example
. The $resultEntityType
is a short string that identifies the type of entity that needs to be generated. In our case, this will be the string interceptor
(we’ll get to where this comes from in a second).
The most compelling bit of code here is $this->_generatedEntities
. This is the array or array-like-object that contains a list of all the different sort of entities Magento can generate. To find out what’s inside lets jump to the generator’s constructor
#File: vendor/magento/framework/Code/Generator.php
public function __construct(
\Magento\Framework\Code\Generator\Io $ioObject = null,
array $generatedEntities = [],
DefinedClasses $definedClasses = null
) {
$this->_ioObject = $ioObject
?: new \Magento\Framework\Code\Generator\Io(
new \Magento\Framework\Filesystem\Driver\File()
);
$this->definedClasses = $definedClasses ?: new DefinedClasses();
$this->_generatedEntities = $generatedEntities;
}
Hmmm — well that’s confusing. The $generatedEntities
parameter does not have an object type hint — it’s just a blank array.
Whenever you see an automatic constructor dependency injection constructor with an array as the default value, it usually means values are provided via di.xml
files. If we search through Magento’s di.xml
file for the Magento\Framework\Code\Generator
type configuration
$ find vendor/magento/ -name di.xml | xargs ack 'Magento\\Framework\\Code\\Generator'
vendor/magento/magento2-base/app/etc/di.xml
637: <type name="Magento\Framework\Code\Generator">
we’ll find the following configuration
#File: vendor/magento/magento2-base/app/etc/di.xml
<type name="Magento\Framework\Code\Generator">
<arguments>
<argument name="generatedEntities" xsi:type="array">
<item name="factory" xsi:type="string">\Magento\Framework\ObjectManager\Code\Generator\Factory</item>
<item name="proxy" xsi:type="string">\Magento\Framework\ObjectManager\Code\Generator\Proxy</item>
<item name="interceptor" xsi:type="string">\Magento\Framework\Interception\Code\Generator\Interceptor</item>
<item name="logger" xsi:type="string">\Magento\Framework\ObjectManager\Profiler\Code\Generator\Logger</item>
<item name="mapper" xsi:type="string">\Magento\Framework\Api\Code\Generator\Mapper</item>
<item name="persistor" xsi:type="string">\Magento\Framework\ObjectManager\Code\Generator\Persistor</item>
<item name="repository" xsi:type="string">\Magento\Framework\ObjectManager\Code\Generator\Repository</item>
<item name="convertor" xsi:type="string">\Magento\Framework\ObjectManager\Code\Generator\Converter</item>
<item name="searchResults" xsi:type="string">\Magento\Framework\Api\Code\Generator\SearchResults</item>
<item name="extensionInterface" xsi:type="string">\Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceGenerator</item>
<item name="extension" xsi:type="string">\Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator</item>
</argument>
</arguments>
</type>
This configuration means Magento will populate the $generatedEntities
parameter with a PHP array that looks something like this
[
'factory'=>'\Magento\Framework\ObjectManager\Code\Generator\Factory',
'proxy'=>'\Magento\Framework\ObjectManager\Code\Generator\Proxy',
'interceptor'=>'\Magento\Framework\Interception\Code\Generator\Interceptor',
'logger'=>'\Magento\Framework\ObjectManager\Profiler\Code\Generator\Logger',
'mapper'=>'\Magento\Framework\Api\Code\Generator\Mapper',
'persistor'=>'\Magento\Framework\ObjectManager\Code\Generator\Persistor',
'repository'=>'\Magento\Framework\ObjectManager\Code\Generator\Repository',
'convertor'=>'\Magento\Framework\ObjectManager\Code\Generator\Converter',
'searchResults'=>'\Magento\Framework\Api\Code\Generator\SearchResults',
'extensionInterface'=>'\Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceGenerator',
'extension'=>'\Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator',
]
These are Magento’s eleven stock code generators.
Picking a Generator
Let’s jump back to our foreach
loop (again, with $className
replaced with our theoretical Pulsestorm\TutorialPlugin\Model\Example\Interceptor
#File: vendor/magento/framework/Code/Generator.php
foreach ($this->_generatedEntities as $entityType => $generatorClass) {
$entitySuffix = ucfirst($entityType);
// If 'Pulsestorm\TutorialPlugin\Model\Example\Interceptor' string ends with $entitySuffix substring
if (strrpos('Pulsestorm\TutorialPlugin\Model\Example\Interceptor', $entitySuffix) === strlen('Pulsestorm\TutorialPlugin\Model\Example\Interceptor') - strlen($entitySuffix)) {
$resultEntityType = $entityType;
$sourceClassName = rtrim(
substr('Pulsestorm\TutorialPlugin\Model\Example\Interceptor', 0, -1 * strlen($entitySuffix)),
'\\'
);
break;
}
}
Inside the foreach
loop, Magento’s code takes the name
attribute from di.xml
($entityType
above), uppercases the first character of the word, and then checks the requested class to see if its suffix matches. It the suffix matches, Magento populates $resultEntityType
with the non-ucfirst
version of the word, a $sourceClassName
is derived by removing the suffix, and the loop breaks — i.e matching stops.
So, in our case, this means there’s no match until Magento gets to the interceptor
key. Then
$entitySuffix = ucfirst('interceptor')
if (strrpos('Pulsestorm\TutorialPlugin\Model\Example\Interceptor', 'Interceptor') === strlen('Pulsestorm\TutorialPlugin\Model\Example\Interceptor') - strlen('Interceptor')) {
$resultEntityType = `Interceptor`;
$sourceClassName = rtrim(
substr('Pulsestorm\TutorialPlugin\Model\Example\Interceptor', 0, -1 * strlen(`Interceptor`)),
'\\'
);
break;
}
Which results in a $sourceClassName
of Pulsestorm\TutorialPlugin\Model\Example
and a $resultEntityType
of interceptor
. These will be important later.
Should I Generate this Class?
Once the first loop is complete, next up is the following code
if ($skipReason = $this->shouldSkipGeneration($resultEntityType, $sourceClassName, $className)) {
return $skipReason;
}
Here Magento passes $resultEntityType
, $sourceClassName
, and $className
to the shouldSkipGeneration
method. If this method returns boolean true
, Magento bails early from generation. If we look at shouldSkipGeneration
‘s definition.
protected function shouldSkipGeneration($resultEntityType, $sourceClassName, $resultClass)
{
if (!$resultEntityType || !$sourceClassName) {
return self::GENERATION_ERROR;
} else if ($this->definedClasses->isClassLoadableFromDisc($resultClass)) {
$generatedFileName = $this->_ioObject->generateResultFileName($resultClass);
/**
* Must handle two edge cases: a competing process has generated the class and written it to disc already,
* or the class exists in committed code, despite matching pattern to be generated.
*/
if ($this->_ioObject->fileExists($generatedFileName)
&& !$this->definedClasses->isClassLoadableFromMemory($resultClass)
) {
$this->_ioObject->includeFile($generatedFileName);
}
return self::GENERATION_SKIP;
} else if (!isset($this->_generatedEntities[$resultEntityType])) {
throw new \InvalidArgumentException('Unknown generation entity.');
}
return false;
}
we see there’s two reasons Magento will skip generating a file. The first is if $resultEntityType
or $sourceClassName
are not populated. This means the foreach
loop ran through every possible generation type, but did not find a matching suffix.
The second is, despite the autoloader being called, the class already exists on disk or in memory.
If either of these cases are true, the Magento autoloader will fail, and PHP will move on to the next autoloader or (in a stock system with only two autoloaders) invoke PHP’s Fatal Error: No Such Class
error/exception.
Generating the Class
We’ve reached the final code block
#File: vendor/magento/framework/Code/Generator.php
$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;
}
In the first line, Magento uses the matched name
from di.xml
to lookup the generator class
#File: vendor/magento/framework/Code/Generator.php
//<item name="interceptor" xsi:type="string">
// \Magento\Framework\Interception\Code\Generator\Interceptor
//</item>
$generatorClass = $this->_generatedEntities['interceptor'];
Each different type of generated entity has a single class dedicated to the job of generating it. For interceptors, this is the Magento\Framework\Interception\Code\Generator\Interceptor
class.
Next up, Magento instantiates an instance of this generator class
#File: vendor/magento/framework/Code/Generator.php
$generator = $this->createGeneratorInstance(
'Magento\Framework\Interception\Code\Generator\Interceptor',
'Pulsestorm\TutorialPlugin\Model\Example',
'Pulsestorm\TutorialPlugin\Model\Example\Interceptor');
//...
protected function createGeneratorInstance($generatorClass, $entityName, $className)
{
return $this->getObjectManager()->create(
$generatorClass,
['sourceClassName' => $entityName, 'resultClassName' => $className, 'ioObject' => $this->_ioObject]
);
}
You’ll notice this is another case of Magento following a do as we say, not as we do w/r/t direct use of the object manager. You’ll also notice that we’re using the create
method of the object manager, meaning a new generator is instantiated every time PHP invokes this autoloader, and that this object’s constructor arguments are passed in using create
‘s second argument.
Finally, if the instantiation worked, Magento calls the tryToLoadSourceClass
method.
#File: vendor/magento/framework/Code/Generator.php
if ($generator !== null) {
$this->tryToLoadSourceClass(
`Pulsestorm\TutorialPlugin\Model\Example`,
$generator);
//...
}
protected function tryToLoadSourceClass($className, $generator)
{
$sourceClassName = $generator->getSourceClassName();
if (!$this->definedClasses->isClassLoadable($sourceClassName)) {
if ($this->generateClass($sourceClassName) !== self::GENERATION_SUCCESS) {
$phrase = new \Magento\Framework\Phrase(
'Source class "%1" for "%2" generation does not exist.',
[$sourceClassName, $className]
);
throw new \RuntimeException($phrase->__toString());
}
}
}
This method makes sure the class we derived with
#File: vendor/magento/framework/Code/Generator.php
$sourceClassName = rtrim(
substr($className, 0, -1 * strlen($entitySuffix)),
'\\'
);
actually exists. We’ll leave an exploration of the details as an exercise for the reader, but there’s one interesting thing to note: If the class does not exist — Magento makes a recursive call to generateClass
in an attempt to generate it. This seems to deal with classes like Foo\Baz\Bar\Bing\Interceptor\Factory
— although it’s unclear how many of these combinations would actually work in the real world.
Generating the Class
If everything in tryToLoadSourceClass
works out Magento attempts to generate the class by calling the generate
method on the specific generator object.
#File: vendor/magento/framework/Code/Generator.php
if (!($file = $generator->generate())) {
$errors = $generator->getErrors();
throw new \RuntimeException(implode(' ', $errors));
}
If the generator produces errors, Magento will bail by throwing a (little used) PHP Standard Library RuntimeException
. Also, if the generator didn’t include the just generated class, Magento will handle that here
#File: vendor/magento/framework/Code/Generator.php
if (!$this->definedClasses->isClassLoadableFromMemory($className)) {
$this->_ioObject->includeFile($file);
}
Generator Class Hierarchy
We’re now able to investigate how an Interceptor
is generated. However, if we jump to the Magento\Framework\Interception\Code\Generator\Interceptor
definition file
#File: vendor/magento/framework/Interception/Code/Generator/Interceptor.php
class Interceptor extends \Magento\Framework\Code\Generator\EntityAbstract
{
//...many methods, none named generate...
}
We’ll see a number of method definitions, but generate
isn’t among them. The Magento\Framework\Interception\Code\Generator\Interceptor
class extends the abstract Magento\Framework\Code\Generator\EntityAbstract
class.
#File: vendor/magento/framework/Interception/Code/Generator/EntityAbstract.php
abstract class EntityAbstract
{
//...
abstract protected function _getDefaultConstructorDefinition();
abstract protected function _getClassMethods();
public function generate()
{
try {
if ($this->_validateData()) {
$sourceCode = $this->_generateCode();
if ($sourceCode) {
$fileName = $this->_ioObject->generateResultFileName($this->_getResultClassName());
$this->_ioObject->writeResultFile($fileName, $sourceCode);
return $fileName;
} else {
$this->_addError('Can\'t generate source code.');
}
}
} catch (\Exception $e) {
$this->_addError($e->getMessage());
}
return false;
}
}
This is the base abstract class that every autoloader based code generator in Magento 2 extends. By defining the _getDefaultConstructorDefinition
and _getClassMethods
methods, as well as defining methods like _validateData
if the need arrises, each individual generator will create its class file.
We will, again, leaves the specifics on the Interceptor’s generation as an exercise for the reader. At this point you should have the tools to track this, or any generated class, down yourself.
Final Takeaways
Before we wrap up for the day, there’s a few key points to review.
First — as of Magento 2.1.0, Magento has eleven generated class types, many of which are undocumented. While it’s possible to have your own classes end in Factory, Proxy, Interceptor, Logger, Mapper, Persistor, Repository, Converter, SearchResults, ExtensionAttributesInterfaceGenerator, or ExtensionAttributesGenerator — you may want to think twice before using these names. You risk stepping on a namespace Magento has (passively) claimed as their own.
Second, Magento’s suffix pattern will catch both
\Foo\Baz\Bar\Interceptor
\Foo\Baz\BarInterceptor
\Foo\Baz\Bar\Factory
\Foo\Baz\BarFactory
It’s up to each individual generator to provide extra validation if they want a particular naming convention.
While Magento 2’s code generation probably violates the old Principle of Least Astonishment it usually just works. For those rare occasions when it doesn’t, we hope the above deep-dive is enough to help you trace the problem to its source.