Magento 2 3/17/2016

Magento 1 and Magento 2: Using Advanced Autoloading

This article is part of a series on design patterns for framework independent code, especially to write extensions for Magento 1 and Magento 2 alike.


All Parts:


Nowadays, not least thanks to the spreading of Composer, almost all PHP projects follow the PSR-4 standard for autoloading, so autoloading should be a no-brainer. If we import our framework-independent library with Composer and the framework includes vendor/autoload.php, then all we need to do is to specify paths for class prefixes in composer.json

Example:

{
  "autoload": {
    "psr-4": {
      "IntegerNet\\Solr\\": "src/Solr/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "IntegerNet\\Solr\\": "test/Solr/"
    }
  }
}
The "psr-4" objects can contain any number of elements and map class prefixes to directories. The trailing backslash is important to only match this exact namespace and its children. Also you need to escape it, so it's a double backslash. The above configuration would map classes like this:
  • IntegerNet\Solr\Exception => [repository]/src/Solr/Exception.php
  • IntegerNet\Solr\Indexer\ProductIndexer => [repository]/src/Solr/Indexer/ProductIndexer.php
"autoload-dev" will be used if you used "composer install --dev" in the repository itself. This configuration also maps classes in /test for development. Note that this is only needed if there are classes that are not executable PHPUnit tests, those are loaded directly by PHPUnit.

Magento 1

Did I say "almost all PHP projects follow the PSR-4 standard"? I should say "all modern PHP projects" because guess who does not! Well, it's not really surprising, given that PSR-4 was introduced 2013, its predecessor PSR-0 in 2010, and Composer was introduced in 2011, while Magento 1.0 was released in 2008.

Good news is: there are modules that add Composer autoload functionality to Magento:

  • EcomDev_ComposerAutoload: replaces Varien_Autoload with a wrapper, rewrites Mage_Core_Model_Cache to ensure it is always loaded
  • Magento-PSR-0-Autoloader: uses observers on early fired events, adds an additional autoloader. No rewrites, but is less reliable (must be included manually for CLI scripts)
Both have their pros and cons, but are good solutions for a Composer based setup. But what if you want to build an extension that is also usable without Composer? Many stores use modman without Composer, or the Magento Connect package manager, or just want to drop a bunch of files into their installation.

A packaged extension would contain all required libraries in the /lib directory. Varien_Autoload also checks this directory for files, but it only works with Snake_Case_Class_Names. And you wouldn't give up real namespaces, right?

So you should ship your Magento 1 extension with a custom autoloader. I'm going to make it optional, so people who use Composer and prefer one of the Composer autoload extensions can turn it off.

Autoloader Code

I used the Class Example from PHP-FIG which serves our purpose, and added a static method createAndRegister():
    public static function createAndRegister()
    {
        if (Mage::getStoreConfigFlag('integernet_solr/dev/register_autoloader')) {
            $libBaseDir = Mage::getStoreConfig('integernet_solr/dev/autoloader_basepath');
            if ($libBaseDir[0] !== '/') {
                $libBaseDir = Mage::getBaseDir() . DS . $libBaseDir;
            }
            self::createAndRegisterWithBaseDir($libBaseDir);
        }
    }
    public static function createAndRegisterWithBaseDir($libBaseDir)
    {
        static $registered = false;
        if (!$registered) {
            $autoloader = new self;
            $autoloader
                ->addNamespace('IntegerNet\Solr', $libBaseDir . '/Solr')
                ->addNamespace('Psr\Log', $libBaseDir . '/Psr_Log')
                ->addNamespace('Psr\Cache', $libBaseDir . '/Psr_Cache')
                ->register();
            $registered = true;
        }
    }
Here I use a configuration value for the base path, "integernet_solr/dev/autoloader_basepath". It defaults to /lib, but in case somebody installs the extension with Composer but does not use a Composer autoload extension yet, they can configure it to be something like "vendor/integernet_solr/src" as well. Also, there is another configuration, "integernet_solr/dev/register_autoloader" which you can use to turn off this autoloader. This one is for those who do use a Composer autoload extension.

This way you have all the flexibility for your preferred install method, but the default values work for most people, especially the not-so-tech-savvy folks. Settings are hidden in a "dev" tab for good reasons.

Autoloader Instantiation

You can put the autoloader class or the creation method somewhere else, that's just a matter of taste. What's important is that you call the method before any class of your libraries is required.

A simple solution is to register the method directly as observer for controller_front_init_before:

<controller_front_init_before>
    <observers>
        <integernet_solr>
            <type>object</type>
                <class>IntegerNet_Solr_Helper_Autoloader</class>
            <method>createAndRegister</method>
        </integernet_solr>
     </observers>
</controller_front_init_before>

In cases where this event is not triggered (yet), like CLI scripts or install scripts, I call it directly with IntegerNet_Solr_Helper_Autoloader::createAndRegister(). The static variable $registered prevents that the autoloader is registered twice.

Bonus: Composer Hack

To make it even easier for people who use Composer without the Composer autoloader, we used a little hack. It's a trade-off between clean separation of the packages and ease of use, but depending on your target audience it's worth considering:

  1. Our own framework independent libraries are configured to be of type "magento-module":
    {
      "name": "integer-net/solr-base",
      "description": "Base library for IntegerNet_Solr open source version",
      "type": "magento-module",
      "require": {
      },
      ...
    }
  2. They also receive a modman file like this:
    src/Solr lib/IntegerNet_Solr/Solr
    lib/Apache/Solr lib/IntegerNet_Solr/Apache_Solr
    lib/Psr/Log lib/IntegerNet_Solr/Psr_Log
    
  3. Finally, all 3rd party requirements are bundled in the repository itself (here: Apache/Solr and Psr/Log)

This way, the Magento Hackathon Composer Installer or any compatible installer moves or links the files directly to /lib, just as in the complete packaged version, and our autoloader works with its default configuration.

If the library is used in a different context, where the type "magento-module" is not recognized, it is treated like the default type "library", which it would normally have. Luckily, Magento 2 uses a different type, namely "magento2-module", so it won't mistake the library for an actual Magento module.


So much for the PSR compliant autoloader. The PSR standards are defined by PHP-FIG, the PHP Framework Interop Group. You might have noticed that I use Psr\Log, a logger interface defined by PSR-3 which is also used by Magento 2. But there's more. The next article covers the PSR interfaces and how we can make use of them.