25/09/2024 Tech
When we start developing with Magento 2, one of the things that can surprise us, is the use of non existing classes in the codebase.
Our development software will be able to point it out if it is not configured correctly.
Theses classes are nevertheless declared by the Magento core itself!
They doesn’t exist after the execution of composer install, but they appear in a generated folder, when executing the di:compile command, or at runtime, as if by magic.
This class generation is closely linked with the design patterns that Magento aimed to implement when Magento 2 was released, such as factory design pattern, the preference design pattern, or proxy design pattern.
For these design patterns, Magento uses some generated classes, such as Proxy, Factory, and Interceptor. These classes are declared in code or in a configuration, but do not exist in the codebase. Each of these class adds a layer of abstraction.
Let’s take the class Factory as an example. It adds a layer of abstraction between the ObjectManager and business code.
The application generates these classes because their named follow a recognized convention: they end with “Factory”.
Here an example of the usage of a Factory class:
public function __construct (
private readonly MagentoCmsModelBlockFactory $blockFactory
) {
}
public function getMyBlock(): BlockInterface
{
return $this->blockFactory->create();
}
So, how are these classes generated?
I decided to delve into the di:compile command, and the proxyGenerator task inside, responsible of the proxy design pattern, and that works with code generator.
If we take a closer look at the ProxyGenerator class, we notice the call to the “class_exists” native php function, in a loop, on the classes collected previously.
/**
* Processes operation task
*
* @return void
*/
public function doOperation()
{
$files = $this->configurationScanner->scan('di.xml');
$proxies = $this->proxyScanner->collectEntities($files);
foreach ($proxies as $entityName) {
class_exists($entityName);
}
}
Looking at the following backtrace, we can notice that the class_exists function refers to the load method of the Autoloader class of Magento code generator:
#0 /var/www/html/setup/src/Magento/Setup/Module/Di/App/Task/Operation/ProxyGenerator.php(55): class_exists()
#1 [internal function]: MagentoFrameworkCodeGeneratorAutoloader->load()
This is the case for 2 reasons:
- Autoloader parameter of class_exist function is not defined, and therefore takes its default value : true. So PHP try to autoload the class if not already loaded.
- Magento Autoloader of custom code generators, was defined and saved as the default autoloader, with “spl_autoload_register”, when the create method of ObjectManagerFactory is used.
/**
* Create class definitions
*
* @return DefinitionInterface
*/
public function createClassDefinition()
{
$autoloader = new Autoloader($this->getCodeGenerator());
spl_autoload_register([$autoloader, 'load']);
return new Runtime();
}
The load method will generate the class if it’s a class, named following recognized convention of Magento, that does not exist in memory or in files.
/**
* Load specified class name and generate it if necessary
*
* According to PSR-4 section 2.4 an autoloader MUST NOT throw an exception and SHOULD NOT return a value.
*
* @see https://www.php-fig.org/psr/psr-4/
*
* @param string $className
* @return void
*/
public function load($className)
{
if (! class_exists($className)) {
try {
$this->_generator->generateClass($className);
} catch (Exception $exception) {
$this->tryToLogExceptionMessageIfNotDuplicate($exception);
}
}
}
There is a generator per type of generated class.
This is the list of all generators available to generate class:
- extensionInterfaceFactory =>MagentoFrameworkApiCodeGeneratorExtensionAttributesInterfaceFactoryGenerator
- factory =>MagentoFrameworkObjectManagerCodeGeneratorFactory
- proxy => MagentoFrameworkObjectManagerCodeGeneratorProxy
- interceptor =>MagentoFrameworkInterceptionCodeGeneratorInterceptor
- logger =>MagentoFrameworkObjectManagerProfilerCodeGeneratorLogger
- mapper => MagentoFrameworkApiCodeGeneratorMapper
- persistor =>MagentoFrameworkObjectManagerCodeGeneratorPersistor
- repository =>MagentoFrameworkObjectManagerCodeGeneratorRepository
- convertor =>MagentoFrameworkObjectManagerCodeGeneratorConverter
- searchResults =>MagentoFrameworkApiCodeGeneratorSearchResults
- extensionInterface =>MagentoFrameworkApiCodeGeneratorExtensionAttributesInterfaceGenerator
- extension =>MagentoFrameworkApiCodeGeneratorExtensionAttributesGenerator
- remote =>MagentoFrameworkMessageQueueCodeGeneratorRemoteServiceGenerator
- proxyDeferred =>MagentoFrameworkAsyncCodeGeneratorProxyDeferredGenerator
In the abstract generator class, a _generateCode method will generate the class with its properties, methods, PHP doc blocks:
/**
* Generate code
*
* @return string
*/
protected function _generateCode()
{
$this->_classGenerator->setName($this->_getResultClassName())
->addProperties($this->_getClassProperties())
->addMethods($this->_getClassMethods())
->setClassDocBlock($this->_getClassDocBlock());
return $this->_getGeneratedCode();
}
Here the Factory class generator with the _getClassMethods method that return all methods for this class: __construct and create methods.
/**
* Returns list of methods for class generator
*
* @return array
*/
protected function _getClassMethods()
{
$construct = $this->_getDefaultConstructorDefinition();
// public function create(array $data = array())
$create = [
'name' => 'create',
'parameters' => [['name' => 'data', 'type' => 'array', 'defaultValue' => []]],
'body' => 'return $this->_objectManager->create($this->_instanceName, $data);',
'docblock' => [
'shortDescription' => 'Create class instance with specified parameters',
'tags' => [
['name' => 'param', 'description' => 'array $data'],
[
'name' => 'return',
'description' => $this->getSourceClassName()
],
],
],
];
return [$construct, $create];
}
If you look the generated/code/Magento/Cms/Model/BlockFactory.php used class at the start of this article, you can see the two methods generated above, and why we can call the create method of this class on the first example of this article.
This is how Magento can generated a certain number of classes at runtime, if they are missing, that allow those design patterns.
Conclusion
By understanding how Magento 2 implements its code generation to manage its design patterns, I can conduct a more precise analysis of my developments which require the use of generated code.
In other words when to use the di:compile command.
You can find more references and the official documentation about the code generation and the Dependency Injection of Magento:
https://developer.adobe.com/commerce/php/development/components/code-generation/
https://developer.adobe.com/commerce/php/development/components/dependency-injection/
I will explain how interceptors for plugins work in an next article, and all the steps of di:compile command in another one.
Magento 2 runtime auto-generated class process explained was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.