Categories


Archives


Recent Posts


Categories


Magento Configuration Lint

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.

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

This entry is part 7 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, and Logging Magento's Controller Dispatch. Later posts include 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, Magento's Global Variable Design Patterns, Magento 2: Factory Pattern and Class Rewrites, 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.

The biggest challenge for Ruby and PHP web developers coming to Magento is having to deal with individual module configuration. This is an integral part of of Magento that won’t ever go away, but I think there’s huge steps we can take to make Magento’s configuration less painful for newcomers.

The System you Have

Convention over Configuration is an old industry war, with an unsteady ceasefire in place between the combatants. Each side agrees to treat the other with silent contempt. I don’t want to go anywhere near a debate that radioactive, but here’s the problem I think we can solve.

Magento’s system is, for better or worse, configuration based. Before you can start writing interesting code you need to write a bunch of boring code (XML) to create the framework for your code to live in. In addition to bringing a different set of assumptions to the table, Magento’s configuration files are also deeply nested and not always intuitive. Some nodes have contextual importance and others do not. There’s probably a handful of people on the planet who know which ones are which. To make matters worse, a partially misconfigured system will run code — until it won’t. This can lead to the incredibly frustrating situation where a developer who’s learning the system isn’t sure if the problem is their own code or if they’ve misconfigured something.

The end result is a cliff of a learning curve that most time-strapped developers don’t have time to climb. Worse, with so much negative feedback early in the learning process, they never get into that groove where they really start learning to work with the system instead of against it.

I don’t want to pick a side in the Convention vs. Configuration fight. I want to make Magento’s configuration process less confusing for newcomers.

What’s a Lint?

The generic term lint comes from a program of the same name. What the original lint program did was scan C code for things that weren’t technically wrong (they’d compile), but were likely not what the original programmer had in mind. The combination of C’s terse semantics and raw power led to countless situations where a simple typo or failure to think through a function could lead to subtle memory errors that took days to track down. The lint program scans your code and looks for common errors that still compile. This is sometimes called static analysis.

To web developers (or at least those web developers paying attention) the most well known lint like program is Douglas Crockford’s JSLint. JSLint lint scans Javascript code for things Doug thinks will lead to errors and/or misunderstood programs. Javascript has some quirks (attempts by the interpreter to “guess” where you wanted a semicolon, scoping issues with for, etc.) that can lead to subtle and hard to track down bugs. JSLint calls these out right away. Again, JSLint will call out code that is valid and will run, but it’s code that may lead to confusing bugs or behavior.

Magento’s configuration system suffers from the same problem. It’s very powerful, but confusing to newcomers. Even those familiar with it can make a quick slip of the fingers leading, once again, to hard to track down bugs. This is why we need a Configuration Lint, and this is why I’ve built a new system for creating Lint Cases.

How Configlint Works

There are two Magento modules in the Github Project. The first, Configviewer, is the system that contains the lint runner as well as a few sample Lint Cases. The second, Linttemplate, is a module with several empty Lint Cases (some passing, some failing) that you can use to start writing new cases right away.

To get started, let’s download and install both the Configviewer and Linttemplate module I prepared for this tutorial. If you’re new here and are unsure on how to install modules, I’d recommend some remedial reading.

Running The Configlint

If you’ve loaded up both modules, head over to the following URL

http://example.com/configlint/

This URL is calling the Configviewer’s IndexController, which contains all the code you need to run your Lint Cases.

File: app/code/local/Alanstormdotcom/Configlint/controllers/IndexController.php

class Alanstormdotcom_Configlint_IndexController extends Mage_Core_Controller_Front_Action
{
    public function indexAction()
    {
        $helper = Mage::helper('configlint/runner')
        ->runLints()
        ->report();
    }        
}

Running the above should net you at least three failures. You may see more, depending on the state of your other modules.

* Class [Alanstormdotcom_Linttemplate_model] does not have proper casing. Each_Word_Must_Be_Leading_Cased. FAILED: Alanstormdotcom_Configlint_Helper_Lints_Xmlstructure::lintClassCase at 111
* FAILED: Alanstormdotcom_Linttemplate_Helper_Lints_Example::lintFailure at 6
* I failed at 31 in Alanstormdotcom_Linttemplate_Helper_Lints_Example::lintFailureWithCustomErrors FAILED: Alanstormdotcom_Linttemplate_Helper_Lints_Example::lintFailureWithCustomErrors at 11 

Let’s take a look at what it takes to write a Lint Case.

Writing Config Lints

Take a look at the following file

app/code/local/Alanstormdotcom/Linttemplate/Helper/Lints/Example.php

Inside you’ll find a Lint Case

class Alanstormdotcom_Linttemplate_Helper_Lints_Example extends Alanstormdotcom_Configlint_Helper_Lints_Abstract
{        
    public function lintFailure($config)
    {
        $this->fail();
    }

    public function lintFailureWithCustomErrors($config)
    {
        $this->fail('I failed at ' . __LINE__ . ' in ' . __METHOD__);
    }

    public function lintPass()
    {
        //the lack of a call to fail indicates we passed
    }        
}

Lint tests are Magento Helper classes that extend the Abstract class

Alanstormdotcom_Configlint_Helper_Lints_Abstract

As of right now there are no abstract methods you’ll need to implement, but that may change in future versions.

Th Lint Runner will scan each Lint Case for a public function that starts with the word lint. These functions will automatically be called, and will also be passed an instance of a Magento Config object. Then, the programmer creating the Lint Case will programatically examine the passed in configuration for anything that looks fishy. If a fishy state is detected, the “fail” method should be called

$this->fail();

This signals to the Lint runner that the config has a suspicious construct. Let’s comment that out

public function lintFailure($config)
{
    //$this->fail();
}

and reload our test runner page

http://example.com/configlint/

you’ll see you have one less failure to worry about.

* Class [Alanstormdotcom_Linttemplate_model] does not have proper casing. Each_Word_Must_Be_Leading_Cased. FAILED: Alanstormdotcom_Configlint_Helper_Lints_Xmlstructure::lintClassCase at 111
* I failed at 31 in Alanstormdotcom_Linttemplate_Helper_Lints_Example::lintFailureWithCustomErrors FAILED: Alanstormdotcom_Linttemplate_Helper_Lints_Example::lintFailureWithCustomErrors at 11 

Turning Lints On and Off

This is Magento, so we’re not going to get by without some configuration. Take a look at the following file

File: app/code/local/Alanstormdotcom/Linttemplate/etc/configlints.xml
<config>
    <lints>
    <unique_identifier>
        <helper_class>linttemplate/lints_example</helper_class>
    </unique_identifier>        

    <unique_identifier_2>
        <helper_class>linttemplate/lints_codepoolcase</helper_class>
    </unique_identifier_2>        
    </lints>
</config>

This config file tells us which classes in our module are Lint Cases and should be scanned. You can specify multiple classes, but each will need it’s own <unique_identifier>

    <lints>
        <unique_identifier>
            <helper_class>linttemplate/lints_example</helper_class>
        </unique_identifier>        
        <unique_identifier_2>
            <helper_class>linttemplate/lints_codepoolcase</helper_class>
        </unique_identifier_2>    
        <foo_bar_bar>
            <helper_class>linttemplate/lints_another</helper_class>
        </foo_bar_bar>        
    </lints>

The <unique_identifier> and <foo_bar_bar> are unique, arbitrarily named nodes. This is a byproduct of the way Magento loads it’s configuration files. Each node needs to have a unique name, but the node names don’t tell the system anything.

The value in <helper_class /> is the URI/Grouped Class Name for a Helper class. Quick review of Grouped Class Names: The following URI

linttemplate/lints_example

translates as

  1. The linttemplate value is used to lookup the Namespace/Module name of Alanstormdotcom_Linttemplate

  2. Because it’s a helper class, the type is added to make Alanstormdotcom_Linttemplate_Helper

  3. Finally, the second portion of the URI is added and we end up with Alanstormdotcom_Linttemplate_Helper_Lints_Example

  4. The __autoload dictates that Alanstormdotcom/Linetemplate/Helpers/Lints/Example.php be included to load the class.

Let’s comment this out to turn off the Lint Cases for this module

<!-- 
    <unique_identifier>
        <helper_class>linttemplate/lints_example</helper_class>
    </unique_identifier>        
-->

If we reload our page there should only be one failure left

*  Class [Alanstormdotcom_Linttemplate_model] does not have proper casing. Each_Word_Must_Be_Leading_Cased. FAILED: Alanstormdotcom_Configlint_Helper_Lints_Xmlstructure::lintClassCase at 111

This failure is coming from one of the default Lint Cases that ships with the Configlint module (not the Linttemplate module we’ve been working with). The configlint/lints_xmlstructure lint looks for classes in the config that have incorrect casing. In this case, it found one

Alanstormdotcom_Linttemplate_model

To save you your acking, this model is in the Linttemplate. Open up

File: app/code/local/Alanstormdotcom/Linttemplate/etc/config.xml

and change

<class>Alanstormdotcom_Linttemplate_model</class>

to use the correct casing (Model, not model)

<class>Alanstormdotcom_Linttemplate_Model</class>

If you clear your Magento cache (for the config change) and reload your test page, the last error should be gone. If the other modules in your system pass the existing lints, you should see a message something like this

All Passed
5 Lint Cases passed

Let’s take a look at that default Line Case that caught this error, it’s at

File: app/code/local/Alanstormdotcom/Configlint/Helper/Lints/Xmlstructure.php
class Alanstormdotcom_Configlint_Helper_Lints_Xmlstructure extends Alanstormdotcom_Configlint_Helper_Lints_Abstract
{            
    protected function setWhichConfig()
    {
        return 'config.xml';
    }    

    /**
    * Tests that all the expected top level xml nodes are in place
    *
    * Doesn't impose that only nodes xyz be in place, it just makes sure
    * the known nodes ARE there
    */        
    public function lintTestTopLevel($config)
    {
        $expected_top     = array('modules','global','frontend','adminhtml','install','default','stores','admin','websites','crontab');
        $xml = simplexml_load_string($config->asXML());
        $found_top         = array();
        foreach($xml as $item)
        {
            $found_top[] = $item->getName();
        }

        //if one of the expected modules is missing, fail
        foreach($expected_top as $node)
        {
            if(!in_array($node, $found_top))
            {
                $this->fail('Could not find [&lt;' . $node . '/&gt;] at the top level. (in ' . 
                __METHOD__ . ' near line ' .
                __LINE__ . 
                ')');
            }
        }
    }    

    /**
    * Classes in configs should be one of four types,
    * Models, Controllers, Blocks, Helpers
    */
    public function lintClassType($config)
    {
        $allowed = array('controller','model','block','helper');
        $nodes = $config->xPath('//class');        
        $errors = array();
        foreach($nodes as $node)
        {
            $str_node = (string) $node;
            if(strpos($str_node, '/') === false && strpos($str_node, '_') !== false)
            {
                $parts = preg_split('{_}',$str_node,4);                    
                if(array_key_exists(2, $parts) && !in_array(strToLower($parts[2]), $allowed))
                {            
                    $errors[] = "Invaid Type [$parts[2]] detected in class [$str_node]";
                }
            }
        }
        if(count($errors) >0)
        {
            $this->fail(implode("\n", $errors));
        }

    }

    /**
    * Tests that all classes are cased properly.  
    *        
    * This helps avoid __autoload problems when working 
    * locally on a case insensatie system
    */        
    public function lintClassCase($config)
    {
        $nodes = $config->xPath('//class');            
        $errors = array();
        foreach($nodes as $node)
        {
            $str_node = (string) $node;
            if(strpos($str_node, '/') !== false)
            {
                if($str_node != strToLower($str_node))
                {
                    $errors[] = 'URI ['.$str_node.'] must be all lowercase;'; 
                }
            }
            else if(strpos($str_node, '_') !== false)
            {
                $parts = preg_split('{_}',$str_node,4);
                foreach($parts as $part)
                {
                    if(ucwords($part) != $part)
                    {
                        $errors[] = "Class [$str_node] does not have proper casing. Each_Word_Must_Be_Leading_Cased.";
                    }
                }
            }
            else
            {
                $errors[] = 'Class ['.$str_node.'] doesn\'t loook like a class'; 
            }
        }

        if(count($errors) > 0)
        {
            $this->fail(implode("\n", $errors));
        }
    }
}

If you take a look at the methods in this class, you can see how an actual Lint Cast might work. Each method in the class will be automatically passed a Magento Config Object, and tests/checks can be run against it.

Wrapup and Three Ways You can Help

This is where I need your help. Right now I’ve shipped with a few simple Lint Cases and I’ll be writing additional ones, but the more common misconfigurations we capture the more useful this tool is. I need your help, here’s what you can do

  1. Download the Module and use it. Keep checking back for updates to the default Lint Cases. Let me know if the cases are catching configurations you feel are valid.
  2. Write new cases and let me know about them. Got a configuration situation that’s always biting you? Capture it in a Lint Case and never let it happen again.

  3. Don’t feel up to writing a Lint Case? Describe your situation in words so someone else can write the case for you! I’ve setup a Stackoverflow Community Wiki question for tracking your misconfigurations.

  4. Know git? Get in touch, because I’m running the project off GitHub to force my cranky old man self to learn a DVCS, and I’ll probably have questions as this this takes off.

Right now the framework for running Lint Cases is simple and not as abstract/robust as some of you might like. However, a more sophisticated Lint Runner and Reporting Engine can come later and we’ll still be able to understand the tests we’re writing now.

If you’ve found my tutorials at all useful I urge you to get involved with this project. Both for the altruistic reasons (helping people is good) and selfish ones (helping yourself is really good).

Like this article? 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.

Originally published March 19, 2010
Series Navigation<< Logging Magento’s Controller DispatchSlides from Magento Developer’s Paradise >>