Categories


Archives


Recent Posts


Categories


Magento 2: Factory Pattern and Class Rewrites

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!

This entry is part 24 of 43 in the series Miscellaneous Magento Articles. Earlier posts include Magento Front Controller, Reinstalling Magento Modules, Clearing the Magento Cache, Magento's Class Instantiation Abstraction and Autoload, Magento Development Environment, Logging Magento's Controller Dispatch, Magento Configuration Lint, Slides from Magento Developer's Paradise, Generated Magento Model Code, Magento Knowledge Base, Magento Connect Role Directories, Magento Base Directories, PHP Error Handling and Magento Developer Mode, Magento Compiler Mode, Magento: Standard OOP Still Applies, Magento: Debugging with Varien Object, Generating Google Sitemaps in Magento, IE9 fix for Magento, Magento's Many 404 Pages, Magento Quickies, Commerce Bug in Magento CE 1.6, Welcome to Magento: Pre-Innovate, and Magento's Global Variable Design Patterns. Later posts include Magento Block Lifecycle Methods, Goodnight and Goodluck, Magento Attribute Migration Generator, Fixing Magento Flat Collections with Chaos, Pulse Storm Launcher in Magento Connect, StackExchange and the Year of the Site Builder, Scaling Magento at Copious, Incremental Migration Scripts in Magento, A Better Magento 404 Page, Anatomy of the Magento PHP 5.4 Patch, Validating a Magento Connect Extension, Magento Cross Area Sessions, Review of Grokking Magento, Imagine 2014: Magento 1.9 Infinite Theme Fallback, Magento Ultimate Module Creator Review, Magento Imagine 2014: Parent/Child Themes, Early Magento Session Instantiation is Harmful, Using Squid for Local Hostnames on iPads, and Magento, Varnish, and Turpentine.

If you’ve been paying attention, you know that Magento 2 is slated for a Q4, 2012 release. While that’s an entire year away, and there’s also the whole Mayan End of Time thing to worry about, it’s never too early to start looking at how some of the discussed changes in the system are being implemented. Today we’ll be looking at the re-architected class rewrite system.

I can’t stress enough that this article covers a development version of Magento. This isn’t even alpha software. Comments are based on a December 2, 2011 git clone of the source. While I hope that all my articles are broad enough to be interesting years later, if you’re coming here looking for help with a Magento 2 problem, relying on the specifics will leaving you weeping on the side of the road.

Disclaiming done, let’s get started.

Magento’s Factory Methods

Magento 2 retains the three model, helper, and block factory methods

Mage::getModel()
Mage::helper()
Mage::getSingleton('core/layout')->createBlock()

However, the class alias strings, such as

catalog/product
cms/page

are gone. Instead, each factory method accepts the name of a PHP class as a string

Mage::getModel('Mage_Catalog_Model_Product');

Magento 2 has done away with the alternate group/class-name alias structure. If you look at a Magento 2 configuration file, you’ll also see they’ve done away with the three nodes for each object type.

<models/>
<helpers/>
<blocks/>

This means you no longer need to “enable” a module for helpers, blocks, or models.

If you’re familiar with Magento’s current rewrite system, your brain’s now telling you that the rewrite system itself must have changed. Magento 1 used the group and class name to search the configuration for a rewrite node. Without this information, how will Magento lookup the class name? Have rewrites gone away completely?

Simplified Rewrites

Fortunately, class rewrites have not gone away, but the configuration syntax has changed.

Magento code modules are still added to the system in the same way. A module is defined by creating an etc/config.xml file in one of the three Magento code pools

app/code/core/Mage/Cms/etc/config.xml
app/code/community/Pulsestorm/Rewriteexample/etc/config.xml
app/code/local/Packagename/Modulename/etc/config.xml

The basic config.xml file should look familiar

<!-- File: app/code/community/Pulsestorm/Rewriteexample/etc/config.xml -->
<config>
    <modules>
        <Pulsestorm_Rewriteexample>
            <version>0.1.0</version>
        </Pulsestorm_Rewriteexample>
    </modules>
</config>

as should the configuration file to enable the module

<!-- File: app/etc/modules/Pulsestorm_Rewriteexample.xml -->
<config>
    <modules>
        <Pulsestorm_Rewriteexample>
            <active>true</active>
            <codePool>community</codePool>
        </Pulsestorm_Rewriteexample>
    </modules>
</config>

Remember, all modules are is how any developer adds their own code to the Magento system. This hasn’t changed from Magento 1. What has changed is the syntax to create a class rewrite. Let’s try rewriting the CMS Page model class.

Mage_Cms_Model_Page

We’ll test our rewrite in a Magento shell script. This will also give us a chance to view the simplified Magento 2 bootstrap.

Magento 2 PHP Bootstrap

To build a stand alone Magento shell script, create the following file in the root of your Magento installation.

#File test_rewrite.php
require_once 'app/bootstrap.php';

/* Store or website code */
$mageRunCode = isset($_SERVER['MAGE_RUN_CODE']) ? $_SERVER['MAGE_RUN_CODE'] : '';
/* Run store or run website */
$mageRunType = isset($_SERVER['MAGE_RUN_TYPE']) ? $_SERVER['MAGE_RUN_TYPE'] : 'store';

Mage::init($mageRunCode, $mageRunType);

//start our script
$cms_page = Mage::getModel('Mage_Cms_Model_Page');
echo get_class($cms_page) . "\n";

This file mimics what’s found in index.php. First, you can see we’re requiring in an app/bootstrap.php file.

require_once 'app/bootstrap.php';

This is where Magento will do the majority of its normalization for a particular PHP systems (ini settings, autoloader init, etc.). This leads to a cleaner index.php, and less cruft at the top of app/Mage.php. Both were problems with the Magento 1 code base.

Next, we check our environment for a store and/or website code, as well as the run type. These can be left as is

/* Store or website code */
$mageRunCode = isset($_SERVER['MAGE_RUN_CODE']) ? $_SERVER['MAGE_RUN_CODE'] : '';
/* Run store or run website */
$mageRunType = isset($_SERVER['MAGE_RUN_TYPE']) ? $_SERVER['MAGE_RUN_TYPE'] : 'store';

Finally, we initialize the Magento environment

Mage::init($mageRunCode, $mageRunType);

The init method loads the Magento environment, which means setting up things like Magento’s custom error handler, loading the config, setting up and reading from caches, etc. After we call init we can use any Magento code as though we were in a controller action or block context (without, of course, access to $this, since we’re still in a PHP global context)

If this were index.php, we’d be calling run instead of init. Starting the Magento environment with run does everything init does, but also triggers any database updates (setup resources/migrations), and instantiates a front controller object and dispatches it (which in turn initializes the Magento URL Routing procedure, eventually resulting in an HTML page being rendered).

With the Magento environment initialized, we’re free to instantiate our page object and check its class

//start our script
$cms_page = Mage::getModel('Mage_Cms_Model_Page');
echo get_class($cms_page) . "\n";

Running the above with a base Magento 2 installation results in the following output

$ /usr/bin/php test_rewrite.php 
Mage_Cms_Model_Page

So far so good. Next, using the implied Pulsestorm_Rewriteexample module from above, we’ll create a new model to use in place of the standard CMS Page model

#File: app/code/community/Pulsestorm/Rewriteexample/Model/Test.php
<?php
class Pulsestorm_Rewriteexample_Model_Test extends Mage_Cms_Model_Page
{
}

and then add the following to our config.xml file to configure the rewrite

#File: app/code/community/Pulsestorm/Rewriteexample/etc/config.xml
<config>
    <!-- ... -->
    <global>
        <rewrites>
            <Mage_Cms_Model_Page>Pulsestorm_Rewriteexample_Model_Test</Mage_Cms_Model_Page>
        </rewrites>
    </global>
    <!-- ... -->
</config>

With the above in place clear your cache (some things never change), and run your script again

$ /usr/bin/php test_rewrite.php 
Pulsestorm_Rewriteexample_Model_Test

This time Magento has instantiated a Pulsestorm_Rewriteexample_Model_Test model. Our rewrite achieved, we could now change any method implementations we wanted to.

Why This Works

Magento 2 has added a new <rewrites/> node to the main configuration tree

<global>
    <rewrites>
        <!-- nodes with class names to rewrite -->
    </rewrites>
</code>

When instantiating a class via the factory helper methods (in our case, Mage_Cms_Model_Page), Magento will look for a node with this class name under the <rewrites/> node. If it finds one, it will use the value of that node as the actual class name

<Mage_Cms_Model_Page>Pulsestorm_Rewriteexample_Model_Test</Mage_Cms_Model_Page>

One thing to note: As of this writing, white space matters here. Don’t configure your XML like this

<!-- Magento 2 does NOT like whitespace here -->
<Mage_Cms_Model_Page>
    Pulsestorm_Rewriteexample_Model_Test
</Mage_Cms_Model_Page>

or PHP will try to instantiate a class with leading white space, which makes it upset.

Another interesting thing to note about Magento 2 is ALL rewrites are configured in the same place. If you wanted to rewrite a helper or block, you’d use

<Mage_Cms_Helper_Data>Pulsestorm_Rewriteexample_Helper_Mydatahelper</Mage_Cms_Helper_Data>
<Mage_Cms_Block_Page>Pulsestorm_Rewriteexample_Block_Thepage</Mage_Cms_Block_Page>

If you look at the configuration class

#File: app/code/core/Mage/Core/Model/Config.php
public function getBlockClassName($blockClass)
{
    return $this->getModelClassName($blockClass);
}

public function getHelperClassName($helperClass)
{
    return $this->getModelClassName($helperClass);
}

public function getBlockClassName($blockClass)
{
    return $this->getModelClassName($blockClass);
}

public function getResourceModelClassName($modelClass)
{
    return $this->getModelClassName($modelClass);
}     

public function getModelClassName($modelClass)
{
    return $this->_applyClassRewrites($modelClass);
}

you can see that all the methods for fetching a particular object’s class name actually point to getModelClassName, which in turn wraps to _applyClassRewrites

Different Approach in Magento 2

Magento 2 has a much simpler class rewrite system. In Magento 1, you had a very abstract system for adding certain class types to a code module. You could then use that abstract system to create class rewrites. With Magento 2, the entire system has become less abstract, and a general class rewriting feature has been added to the system. It’s a subtle difference, but reflects the state of the Magento project and ecosystem.

Four years ago, the core team’s mission (or, for the “creatives” out there, the brief) was to create the most flexible system possible. Build something that, no matter how complex, would allow merchants to build out the store they wanted. Leave no feature un-changeable. That’s the context of Magento 1.

Now, with Magento having (to a large extant) achieved it’s goals, the engineering is focused on addressing problems that crop up in common usage. The system no longer exists in a void, and the engineering is now focused on addressing common user requests and complaints. THe engineering team and leadership also have a better understanding of the problem space. Because of that, it’s natural that Magento 2 will simplify formerly complex systems.

Of course, because it’s the Magento Core team, there will probably new a plethora of NEW complicated system as well!

Magento 2 Autoload

If you think we’re wrapping things up, you’re wrong! If you’ve been following along with Magento 2 development, you’ve probably heard that features are being added to the system to address the “multiple rewrite” problem. This is where two modules attempt to rewrite the same class.

As the rewrite system currently works, one class always wins. This has left store owners to resolve that inheritance chain themselves, which means editing distributed module files. This can lead an inexperienced store owner down a dangerous path, and leave an extension distributer burning cycles supporting jiggered with files. Not a happy solution for anyone.

The Magento 2 rewrite system, while simpler, doesn’t do a thing to address the multiple re-write problem. The quick and easy conclusion would be that the core team is just making things up to keep people quiet. Fortunately, from what I see, that’s not the case. Magento 2 features a completely overhauled autoloader which will help users resolve these conflicts themselves in a way that leaves extensions unmolested.

The Magento_Autoloader

If you look at the bootstrap file

#File app/bootstrap.php       
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'local';
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'community';
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'core';
$paths[] = BP . DS . 'lib';

Magento_Autoload::getInstance()->addIncludePath($paths)
    ->addFilesMap(BP . '/_classmap.php');

you can see that Magento’s replaced the Varien_Autoload class with a new Magento_Autoload class. The autoloader’s addIncludePath method simply adds the passed in paths to the PHP include path. In this way, the autoloader is implementing code pools in the same way as Magento 1.

#File: lib/Magento/Autoload.php
public function addIncludePath($path)
{
    if (!is_array($path)) {
        $path = array($path);
    }
    $path[] = get_include_path();
    set_include_path(implode(PATH_SEPARATOR, $path));
    return $this;
}     

However, there’s also the addFilesMap method, which has a less clearly defined goal. If we look at the definition

#File: lib/Magento/Autoload.php
public function addFilesMap($map)
{
    if (is_string($map)) {
        if (is_file($map) && is_readable($map)) {
            $map = include $map;
        } else {
            throw new Exception($map . ' file does not exist.');
        }
    }
    if (is_array($map)) {
        $this->_filesMap = array_merge($this->_filesMap, $map);
    } else {
        throw new Exception('$map parameter should be an array or path map file.');
    }
    return $this;
}     

we can see that it includes a file that returns an array, and then assigns that array to the internal _filesMap property. If we take a look at the _classmap.php file that ships with Magento 2

return array (
);

we can see it’s returning an empty array. (Did you know you can return results from an include?).

So far that all makes sense, but doesn’t seem to do anything. However, if we search the class file for _filesMap, we find this

#File: lib/Magento/Autoload.php
public function autoload($class)
{
    if (isset($this->_filesMap[$class])) {
        $classFile = $this->_baseDir . $this->_filesMap[$class];
    } else {
        $classFile = $this->_getClassFile($class);
    }         
    require $classFile;
}

The format of the files map appears to be a PHP array, indexed by class name. When the autoloader attempts to load a specific class (Pulsestorm_Rewriteexample_Model_Test), if there’s a key defined for this class in the files map, the value associated with this key will be used as the require path.

This means that, effectively, what the files map autoloader feature does is allow you to override the autoloader’s behavior on a class by class basis. The existing documentation for this hints that this feature is intended as a performance improvement and replacement for the class compilation feature of Magento 1, but with a little creative thinking it could also be used on an installation by installation basis to allow store owners to change the inheritance chain for modules with conflicting rewrites.

For example, if you add the following to app/_classmap.php

#File: app/_classmap.php
return array (    
    'Pulsestorm_Rewriteexample_Model_Test'=>'app/code/local/rewrite-bootstrap/example.php' 
);      

and run your script, you’ll see error output something like this

$ php test_rewrite.php
Fatal error: Magento_Autoload::autoload(): Failed opening required 'app/code/local/rewrite-bootstrap/example.php'

However, if we add the following file

#File: app/code/local/rewrite-bootstrap/example.php
<?php
class Pulsestorm_Rewriteexample_Model_Test extends Mage_Cms_Model_Page
{
}

our script is restored, and the class in example.php is loaded instead.

$ php test_rewrite.php 
Pulsestorm_Rewriteexample_Model_Test

More importantly, we could do something like this

#File: app/code/local/rewrite-bootstrap/example.php
<?php
class Pulsestorm_Rewriteexample_Model_Test extends Otherpackage_Theirmodule_Model_Page
{
}     

if there was an Otherpackage_Theirmodule_Model_Page class that was also attempting to rewrite the CMS page class. While we still need to redefine the top level object, this can be done without having to edit the module distribution files. In the case of longer inheritance chains, a _classmap.php like this

#File: app/_classmap.php
return array (    
    'Pulsestorm_Rewriteexample_Model_Test'=>'app/code/local/rewrite-bootstrap/example.php' 
    'Otherpackage_Theirmodule_Model_Page'=>'app/code/local/rewrite-bootstrap/example.php'
    'Thirdpackage_Theirmodule_Model_Page'=>'app/code/local/rewrite-bootstrap/example.php'
);      

with a “rewrite bootstrap” like this

//second paramater ensure 
if(!class_exists('Pulsestorm_Rewriteexample_Model_Test', false))
{
    //definitions to resolve conflicts here.  PHP allows
    //conditional class definitions
}

will allow users (or extension distributors) to create files that work around clashing rewrites. In case you missed it, here our files map indicates the same file should be used for multiple classes, allowing us to keep all our changes in one place).

Most importantly, the files map can be manipulated at run-time

$map = include BP . '/_classmap.php';
//change the map
$map['Foo_Bar_Model_Baz'] = 'path/to/rewrite/bootstrap.php';
Magento_Autoload::getInstance()->addIncludePath($paths)
    ->addFilesMap(BP . '/_classmap.php');

which is how I’m predicting any UI for managing class rewrite conflicts will work.

Everything’s a Guess

Of course, at this point there’s not such thing as a rewrite bootstrap, this is all idle speculation. Magento 2 is at least a year away from release. It’s uncertain what features will make it into the final system, and what sort of product will ultimately emerge.

While Magento itself is showing an unprecedented level of transparency via conferences and the Magento 2 wiki, I’m hoping the community of Magento developers will start poking at Magento 2 and writing about it sooner rather than later. The more we explore, prod, bend, and break the system now, the more we help shape the final product, and help the agents of transparency within both Magento and eBay.

Originally published December 5, 2011
Series Navigation<< Magento’s Global Variable Design PatternsMagento Block Lifecycle Methods >>

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 5th December 2011

email hidden; JavaScript is required