Categories


Archives


Recent Posts


Categories


Magento 2 Autoloader and Class Generation

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.

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

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.phpMagento 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

  1. Scan the class name for certain appended strings
  2. 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
  3. Use that generator class to generate a boilerplate class
  4. 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

  1. Checks if the class name is one Magento needs to generate
  2. Checks if there’s a reason we shouldn’t generate the class
  3. 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.

Originally published August 31, 2016

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 31st August 2016

email hidden; JavaScript is required