All about Magento E-commerce Store.......MagentoForum: Magento Configuration Lint

Friday, September 16, 2011

Magento Configuration Lint


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 ofAlanstormdotcom_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 withAlanstormdotcom_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 theLinttemplate 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).

No comments:

Post a Comment