Looking for something?

Magento ships with a powerful, robust, and somewhat undocumented XML-driven back-end system configuration solution.  This article assumes that the reader is well acquainted with this system.  If not, a prerequisite crash-course can be found in one of Alan Storm's excellent articles.  This article looks beyond what the system can do and reaches into what the software developer should do with it in order to best serve their end-users.

By the end of this article, the reader should feel confident that his or her system configurations are bulletproof.  Please bear in mind that this article is going to seem a little "preachy", but there are some technical tidbits scattered throughout.

What makes a configuration system intuitive?

Use Proper Grammar, Punctuation, and Terminology for Communicating with Your Target Audience.

This is arguably one of the quickest and easiest ways to boost the effectiveness of a module. Unfortunately, due to the influx of third party code emerging from countries whose developers are not primary speakers of the languages of some of their largest customers, there seems to be somewhat of a language gap. This most often manifests in obvious misspellings, incomplete phrases, and most often the use of inappropriate terminology.  It is not a safe assumption that the end-users of a module will be at all familiar any or all of the following terms: pagination, regex, csvproxy, cron, ttl, etc...  The end-user is most likely going to be a business-oriented individual and not a programmer.  Terms that may seem commonplace to development staff should be explained to end users more thoroughly than through the use of a simple acronym.

Set Reasonable Default Values

When applicable, always strive to fill out as much of the configuration as possible for the user.  By convention, default system configuration values are defined within a module's "config.xml" file within the <default> block.  Fields defined within the block follow the exact same structure as defined within the module's "system.xml" file.

If Manual Configuration is Required Before First Use, MAKE IT OBVIOUS

Utilizing the built-in notification toolbar or can prove to be an intuitive way to tell your users that something isn't quite right.  This can be accomplished via an observer.

public function controller_action_layout_generate_blocks_after() {
    
    /// Only divulge details of the misconfiguration to authenticated parties
    if (Mage::getSingleton('admin/session')->isLoggedIn()) {
        
        $notification = Mage::app()->getLayout()->getBlock('notifications');
        
        /// Check to make sure someone didn't do something odd with their administrator panel layout
        if($notification) {
            
            /// Check to make sure the extension is properly configured (you will have to write this function)
            $errors = Mage::helper('stabilis')->getSystemConfigErrors();
            
            if($errors) {
                /// Add a notification to the text_list block if any errors were found
            }
        }
    }
}

Don't Overwhelm Your Users: Compartmentalize Your Module

There's nothing worse than opening up a new configuration page and staring blankly at dozens, if not hundreds of random configuration fields packed into a single group.  It can be very overwhelming for store administrators and is usually avoidable by properly compartmentalizing each moving part of a module.  Magento offers enough flexibility in its configuration system to accommodate this.  Perhaps most importantly, when setting up system configuration it's imperative to see the everything through the eyes of the end-user.

Utilize The Layout Hierarchy Effectively

As the reader is probably aware, the Magento system configuration layout hierarchy starts with tabsTabs are broken down into sections, which are further broken down into groups, which are further broken down into fields.

As a general rule of thumb, if a module is adding functionality to an existing system, then new configurations should be plumbed into the existing tabs and/or sections.  For example, if a module adds a new shipping method, then it would be logical and proper to place the new shipping method configuration group under the sales tab, carriers section and not in its own custom tab and/or section.  Many stores have dozens of third party modules installed from a multitude of vendors.  Imagine if each of these vendors were to have added their own tab to the layout.  Now the user is responsible for mapping a company or brand to a specific functionality instead of going straight for the already-existing tab/section responsible for said functionality.

If however a module introduces completely new functionality that is unrelated to any existing system, then it would be permissible to make a new tab and/or section.

Utilize the Single Responsibility Principle with Groups

A group should configure a discrete compartmentalized unit of its module.  Expanding on the shipping method example from earlier, imagine that the module introduces three new shipping methods.  Each method should have its own group whose fields should not affect the functionality of features that are configured by other groups.

Present the Correct Information Using the Correct Mechanism

Taking full advantage of the configuration framework can be an easy way to put your documentation (you know, the kind that most users never bother to read and end up opening support tickets about) in the spotlight.  There are three pieces of information that should be presented with each non-trivial field: a brief label, a descriptive comment, and an example.  As luck would have it, there are also three (actually, 4 -- but we'll get to that...) built-in mechanisms for presenting this information:

  1. <label>
    label example
    Labels take on the responsibility of briefly and adequately describing the field that is being configured.  This is the first piece of information that users will see.  For times when a label is simply not enough, there exists the <comment> tag to help out.
  2. <comment>
    note example
    Comments take over in situations where labels simply do not provide enough real-estate to portray the intricacies of the field being configured.  Comments should cover need to know information or important recommendations regarding the field.  For situations where additional information is not mission-critical or there are other hints that may be offered, there enters the <tooltip> tag.
  3. <tooltip>
    tooltip example
    Tooltips serve as a somewhat more subtle means by which to communicate with the end user than do comments.  Tooltips should contain non-mission-critical information, hints, and other information that the normal end-user will not generally have to worry about.
    Tooltips also serve as an excellent place to present an example.  After all, if the user is compelled to view the tool-tip message, then they most likely require more information in order to complete their configuration.  An example is arguable the most useful piece of information that can be offered.
  4. <hint> - As Alan Storm has pointed out, there actually (sort of) exists a fourth mechanism by which the original Magento core developers had started to plumb into the system, however it was seemingly abandoned.  Due to the default adminhtml styling, this field is never visible to the end-user although it is rendered.
    System Configuration Hint Field
    Due to the incomplete nature of this feature, this author cannot endorse the use of this field for any practical purpose. It is covered here only for the sake of completeness.

 Only Show What You Have To

Magento ships with a flexible and easy to use field dependency system.  This system is designed to conditionally show or hide fields depending on the values of other fields.  When properly implemented, this feature allows for a very user-friendly experience.  Dependencies are set up through the use of <depends> tags.

Basic Dependency Example

Only show field2 if the value of field1 is 1.

<!-- ... -->
<fields>
    <field1 translate="label">
        <label>Field 1</label>
        <type>select</type>
        <source_model>adminhtml/system_config_source_yesno</source_model>
    </field1>
    <field2 translate="label">
        <label>Field 2</label>
        <type>text</type>
        <depends>
            <field1>1</field1>
        </depends>
    </field2>
</fields>

Dependencies on Multiple Fields

Only show field3 if field1 is "1" AND field2 is "1".

<!-- ... -->
<fields>
    <field1 translate="label">
        <label>Field 1</label>
        <type>select</type>
        <source_model>adminhtml/system_config_source_yesno</source_model>
    </field1>
    <field2 translate="label">
        <label>Field 2</label>
        <type>select</type>
        <source_model>adminhtml/system_config_source_yesno</source_model>
    </field2>
    <field3 translate="label">
        <label>Field 3</label>
        <type>text</type>
        <depends>
            <field1>1</field1>
            <field2>1</field2>
        </depends>
    </field2>
</fields>

Dependencies on Multiple Values

Show field2 if field1 is "1" OR "true" OR field1 is "platypus".  Note that the separator attribute can be any user-defined character sequence (meaning that it may be more than one character in length).

<!-- ... -->
<fields>
    <field1 translate="label">
        <label>Field 1</label>
        <type>text</type>
    </field1>
    <field2 translate="label">
        <label>Field 2</label>
        <type>text</type>
        <depends>
            <field1 separator="|">1|true|platypus</field1>
        </depends>
    </field2>
</fields>

Dependencies on Multiple Fields AND Multiple Values

Show field3 if field1 is "1" AND (field2 is "1" OR field2 is "true" OR field2 is "platypus")

<!-- ... -->
<fields>
    <field1 translate="label">
        <label>Field 1</label>
        <type>select</type>
        <source_model>adminhtml/system_config_source_yesno</source_model>
    </field1>
    <field2 translate="label">
        <label>Field 2</label>
        <type>text</type>
    </field2>
    <field3 translate="label">
        <label>Field 3</label>
        <type>text</type>
        <depends>
            <field1>1</field1>
            <field2 separator="|">1|true|platypus</field2>
        </depends>
    </field2>
</fields>

Internationalize to the Target Consumers

As a final, minor point, Magento ships with a robust translation system that should be utilized wherever possible.  Such a system allows for easily installing additional language packs as the need arises.  From what this author has seen, most modules on the market already utilize the translation system quite well.

Lost In Translation: Dangerously!  An Industry Example

Now let's look at an example.  The following particular field has been slightly modified to prevent proprietary licensing violations and is from a third-party module that this author has had the dubious pleasure of working with over the past.

<export_order_name translate="label">
    <label>Name Export Order</label>
    <frontend_type>text</frontend_type>
    <sort_order>30</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>0</show_in_store>
</export_order_name>

Looking at the above field, there are a number of things wrong.

1) The end-user has absolutely no idea what to actually type into this field.  What does 'Name Export Order' mean?  Should this be the name of the customer whose order to export?  An arbitrary label that will be displayed on the export?  The file-name of the export?  If it's a file name, is a dispersion mechanism enabled?  Where is the file name relative to?  Is it allowed to enter a fully qualified path?  If not, where is the path relative to?

2) There is no client-side input validation whatsoever.  As far as Magento is concerned, the end-user can enter a single NUL (0x0) character into the field and happily save it.  Hello null byte injection attacks!  What happens if the user enters in "/etc/passwd" and their server isn't properly configured?  Ouch.

3) For the record, there was also zero server-side input validation for any input fields.  Let's hope that the organization running this module doesn't ever encounter a disgruntled employee that knows how to use Firebug or a similar tool.

If the reader is scratching their head right now, that's understandable.  The above field was actually trying to prompt the user for the path relative to the configured 'var/export' folder that exported orders should be saved to.

To improve the preceding example (without changing the behavior of the module business logic), this author would rewrite the configuration XML as follows:

<export_order_name translate="label comment tooltip">
    <label>Order Export Path</label>
    <comment><![CDATA[This should be the path (relative to your configured "<strong><em>export</em></strong>" directory) that exported purchase orders should be saved.]]></comment>
    <tooltip>Please note that if the specified path does not exist, the system will attempt to create it.  In the event that the path is not writable, then the administrator will be notified upon saving this field.</tooltip>
    <validate>validate-path</validate>
    <backend_model>stabilis/system_config_backend_writableExportPath</backend_model>
    <frontend_type>text</frontend_type>
    <sort_order>30</sort_order>
    <show_in_default>1</show_in_default>
    <show_in_website>1</show_in_website>
    <show_in_store>0</show_in_store>
</export_order_name>

 Notice in the above rewrite:

  1. The <label> has been changed to better communicate what is expected.
  2. There is a <comment> that dissolves all ambiguity that the <label> may have created.  Please also note that the content of the comment is wrapped in a CDATA section, as it contains content that should not be considered XML markup.
  3. There is a <tooltip> that provides additional information should the user still encounter problems.
  4. There is a client-side validation class.
    Validation.add('validate-path', 'The provided path contains illegal characters.', function(value) { return value.match(/^[^\0]*$/); }
  5. There is a server-side backend model applied to the user input that filters any data before it is inserted into the database. (NEVER TRUST THE CLIENT!  Even if they're an administrator!)
<?php

class Stabilis_Bulletproof_Model_System_Config_Backend_WritableExportPath extends Mage_Core_Model_Config_Data {

    /** @var string event constant for when a path is not writable */
    const STABILIS_SYSCONFIG_PATH_NOT_WRITABLE = 'stabilis_sysconfig_path_not_writable';
    
    /**
     * Retrieves the filesystem permission mask that should be applied to newly created <strong>directories</strong>
     * 
     * @return int
     */
    protected function _getPermissionMask() {
        return 0755;
    }
    
    /**
     * Retrieves the session that any warning messages should be added to
     * 
     * @return Mage_Core_Model_Session_Abstract
     */    
    protected function _getSession() {
        return Mage::getSingleton('adminhtml/session');
    }
    
    /**
     * Ensures that the provided path is writable by the system.
     * 
     * If the provided path does not exist, this method will attempt to create it.
     * 
     * @param string $path
     * 
     * @throws Mage_Core_Exception if the provided path leads to a file, is not writable, or cannot be created
     */
    protected function _ensureWritable(string $path) {
        if(file_exists($path)) {
            if(is_file($path)) {
                Mage::throwException(Mage::helper('stabilis')->__('A file already exists at specified path (%s).', $path));
            } else if(!is_writable($path)) {
                Mage::throwException(Mage::helper('stabilis')->__('The specified path (%s) is not writable.', $path));
            }
        } else if(!mkdir($path, $this->_getPermissionMask(), true)) {
            Mage::throwException(Mage::helper('stabilis')->__('The specified path (%s) could not be created.', $path));
        }
    }
    
    /**
     * Executes before saving the associated system configuration field.  This method may add a 
     * warning to the administrator session if the underlying path is not writable.
     */
    protected function _beforeSave() {

        /// Remove all NUL (0x0) bytes not appearing at the beginning or end of the trimmed value
        $value = str_replace("\0", '', trim($this->getValue()));

        /// The absolute path that is being configured
        $path = Mage::getBaseDir('export') . DS . $value;

        /// If PHP is configured with an open_basedir directive, then filesystem operations can emit an E_WARNING.
        /// Rather than (ab)using the @ operator, we will elegantly handle these cases.
        set_error_handler(function($errno, $errstr) use($value) {
            Mage::throwException(Mage::helper('stabilis')->__('PHP could not access the specified path (%s).  Additionally, errno %s was emitted as follows: %s', $errno, $value, $errstr));
        }, E_WARNING);

        try {
            
            /// Run the path through all sorts of validations
            $this->_ensureWritable($path);
            
        } catch (Mage_Core_Exception $ex) {
            
            /// If anything goes wonky, add a warning message to the administrator session object
            $this->_getSession()->addWarning($ex->getMessage());
            
            /// Dispatch an event -- third parties may wish to add hints regarding how to fix the situation
            Mage::dispatchEvent(self::STABILIS_SYSCONFIG_PATH_NOT_WRITABLE, array('path' => $path, 'error' => $ex));
        }

        /// No matter what, restore the old error handler
        restore_error_handler();
        
        /// Always call through to the parent class implementation
        return parent::_beforeSave();
    }

}

Wrapping Things Up

Most people that work with me think that I'm a paranoid, persnickety, old stickler when it comes to configuration details.  That's because I am.  I believe that a properly designed configuration system makes it impossible for users to cause a system to misbehave in a way that is not handled by the application.  This usually involves utilizing both front-end validation classes to politely tell the user that something's wrong in addition to back-end validation mechanisms to enforce the rules with an iron fist.  I've always found it very peculiar that so many third party Magento extensions rely so much on flimsy client-side validation.

Additionally, due to the vast facilities and functionality bundled with the Magento configuration system, I also believe that it's very possible that the system configuration UI itself can be a primary source of user documentation.  Putting the details on the page is the most direct way of forcing users to Read The Fantastic Manual.

Anyhow, I'm done preaching.  Go code something!