Mini Tutorial: Adding REGEX Validation to Custom Options

Recently while I was working on a project for a client I was tasked with an unique challenge, adding Regular Expression (also known as regex) validation to a custom option. My first thoughts were that this wouldn’t be to difficult, just get the reg-ex from the client, and use the built in validation that comes stock with Magento, however the client threw in a curve ball. The requirement was to specify the reg-ex and error message individually on each custom option in the back-end. I thought this was an interesting challenge and figured others might to, so I decided to share the solution I came up with.

Overview:

The changes needed to accomplish this task can be divided into two of Magento’s common areas: the back-end and the front-end.

*For the purpose of this post and based on the original requirements that led to this, I have chosen to only implement the validation for the ‘text’ type of custom option, however I do not believe it would be difficult to extend this out to cover the other types of fields if needed.

Back-end: In the back-end I have added two new attributes to Magento’s custom option object: ‘regex’ (used to hold the raw regular expression) and ‘regex_message’ (used to hold the error message that is displayed on the front-end when a field fails validation). This will allow an admin user to optionally enter the regex they wish to use and a ‘fail’ message for any custom option where the input is text (either text field or text area).

customoptions2

Front-end: For the front-end I have added a custom layout and template file and set it up to use as a new renderer specifically for the options of the type ‘text’. Essentially what occurs is for each custom option that has a value specified for the ‘regex’ attribute on the backend we take advantage of Magento’s built in validation and add a new unique validation entry. This allows for each field on the page to get validated through our custom regular expression and through the normal means (required fields, max character length etc.) without conflict.

customoptions1

The Code:

First we need to declare our module (Please note I have chosen to put this module in the community code pool as I am distributing it, however if you were doing a specific customization for a client, you may choose to use the local code pool):

app/etc/modules/Demac_CustomOptionsRegex.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Demac_CustomOptionsRegex>
            <active>true</active>
            <codePool>community</codePool>
        </Demac_CustomOptionsRegex>
    </modules>
</config>

Next lets create the config.xml

app/code/community/Demac/CustomOptionsRegex/etc/config.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Demac_CustomOptionsRegex>
            <version>0.1.0</version>
        </Demac_CustomOptionsRegex>
    </modules>
    <global>
        <blocks>
            <!--Declare our block name-->
            <demac_customoptionsregex>
                <class>Demac_CustomOptionsRegex_Block</class>
            </demac_customoptionsregex>
            <!--Rewrite the adminhtml custom options block to add our new fields-->
            <adminhtml>
                <rewrite>
                    <catalog_product_edit_tab_options_option>Demac_CustomOptionsRegex_Block_Adminhtml_Catalog_Product_Edit_Tab_Options_Option</catalog_product_edit_tab_options_option>
                    <catalog_product_edit_tab_options_type_text>Demac_CustomOptionsRegex_Block_Adminhtml_Catalog_Product_Edit_Tab_Options_Type_Text</catalog_product_edit_tab_options_type_text>
                </rewrite>
            </adminhtml>
        </blocks>
        <resources>
            <!--Setup information for our install script which will allow us to save and retrieve our new custom option attributes-->
            <demac_customoptionsregex_setup>
                <setup>
                    <module>Demac_CustomOptionsRegex</module>
                </setup>
            </demac_customoptionsregex_setup>
        </resources>
    </global>
    <frontend>
        <layout>
            <updates>
                <!--Frontend layout declaration-->
                <demac_customoptionsregex>
                    <file>demac_customoptionsregex.xml</file>
                </demac_customoptionsregex>
            </updates>
        </layout>
    </frontend>
</config>

Now we add an install script to add our new fields to hold the regex data

app/code/community/Demac/CustomOptionsRegex/sql/demac_customoptionsregex_setup/install-0.1.0.php

<?php

/* @var $installer Mage_Core_Model_Resource_Setup */

$installer = $this;

$installer->startSetup();

$optionTable = $installer->getTable('catalog/product_option');

//Add a new columns to the product options table to hold our regex 'attribute' data
$installer->getConnection()
    ->addColumn($optionTable,'regex',
        array(
            'type'      => Varien_Db_Ddl_Table::TYPE_TEXT,
            'length'    => 250,
            'nullable'  => true,
            'comment'   => 'RegEx'
        )
    );

$installer->getConnection()
    ->addColumn($optionTable,'regex_message',
        array(
            'type'      => Varien_Db_Ddl_Table::TYPE_BLOB,
            'nullable'  => true,
            'comment'   => 'RegEx Message'
        )
    );

$installer->endSetup();

Next we need to add two blocks

We created rewrites for these above in config.xml. Option.php to add our new values into the data loaded in the admin custom options tab, and Text.php to add the actual fields to edit/display the new regex fields.
app/code/community/Demac/CustomOptionsRegex/Block/Adminhtml/Catalog/Product/Edit/Tab/Options/Options.php

<?php

class Demac_CustomOptionsRegex_Block_Adminhtml_Catalog_Product_Edit_Tab_Options_Option extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Options_Option {

    public function getOptionValues()
    {
        $values = parent::getOptionValues();
        $optionsArr = array_reverse($this->getProduct()->getOptions(), true);

        //Add our regex attribute data
        foreach($values as $value){
            $option = $optionsArr[$value->getOptionId()];
            $value->setRegex($option->getRegex());
            $value->setRegexMessage($option->getRegexMessage());
        }

        $this->_values = $values;

        return $this->_values;
    }
}

and

app/code/community/Demac/CustomOptionsRegex/Block/Adminhtml/Catalog/Product/Edit/Tab/Options/Type/Text.php

<?php

class Demac_CustomOptionsRegex_Block_Adminhtml_Catalog_Product_Edit_Tab_Options_Type_Text extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Options_Type_Abstract {

    public function __construct()
    {
        parent::__construct();
        //Set the template to our's so we can add the additional fields for the regex information
        $this->setTemplate('demac/customoptionsregex/catalog/product/edit/options/type/text.phtml');
    }
}

That’s it for the backend code, now lets add the adminhtml template we set above (this is a copy of the default template with a couple of additions, the import parts are between the START/END comments)
app/design/adminhtml/default/default/template/demac/customoptionsregex/catalog/product/edit/options/type/text.phtml

<script type="text/javascript">
//<![CDATA[
OptionTemplateText = '<table class="border" cellpadding="0" cellspacing="0">'+
        '<tr class="headings">'+
            <?php if ($this->getCanReadPrice() !== false) : ?>
            '<th class="type-price"><?php echo Mage::helper('catalog')->__('Price') ?></th>' +
            '<th class="type-type"><?php echo Mage::helper('catalog')->__('Price Type') ?></th>' +
            <?php endif; ?>
            '<th class="type-sku"><?php echo Mage::helper('catalog')->__('SKU') ?></th>'+
            '<th class="type-max"><?php echo Mage::helper('catalog')->__('Max</br >Characters') ?> </th>'+

            //START custom labels added by Demac
            '<th class="type-normal"><?php echo Mage::helper('catalog')->__('Regex') ?> </th>'+
            '<th class="type-textarea"><?php echo Mage::helper('catalog')->__('Regex Message') ?> </th>'+
            //END custom labels added by Demac

    '</tr>'+
        '<tr>'+
            <?php if ($this->getCanReadPrice() !== false) : ?>
            '<td><input type="text" class="input-text validate-number product-option-price" id="product_option_{{option_id}}_price" name="product[options][{{option_id}}][price]" value="{{price}}"<?php if ($this->getCanEditPrice() === false) : ?> disabled="disabled"<?php endif; ?>></td>' +
            '<td><?php echo $this->getPriceTypeSelectHtml() ?>{{checkboxScopePrice}}</td>' +
            <?php else : ?>
            '<input type="hidden" name="product[options][{{option_id}}][price]">' +
            '<input type="hidden" name="product[options][{{option_id}}][price_type]" id="product_option_{{option_id}}_price_type">' +
            <?php endif; ?>
            '<td><input type="text" class="input-text" name="product[options][{{option_id}}][sku]" value="{{sku}}"></td>'+
            '<td class="type-max"><input type="text" class="input-text validate-zero-or-greater" name="product[options][{{option_id}}][max_characters]" value="{{max_characters}}"></td>'+

            //START custom inputs added by Demac
            '<td class="type-normal"><input type="text" class="input-text" name="product[options][{{option_id}}][regex]" value="{{regex}}"></td>'+
            '<td class="type-textarea"><textarea class="input-text" name="product[options][{{option_id}}][regex_message]" >{{regex_message}}</textarea></td>'+
            //End custom inputs added by Demac

        '</tr>'+
    '</table>';

if ($('option_panel_type_text')) {
    $('option_panel_type_text').remove();
}
//]]>
</script>

And lastly we need to add our front-end layout xml file, and our front-end template file to display and take advantage of our new regex validation functionality.
app/design/frontend/base/default/layout/demac_customoptionsregex.xml

<?xml version="1.0"?>
<layout version="0.1.0">

    <catalog_product_view>

        <reference name="product.info.options" as="product_options">
            <action method="addOptionRenderer">
                <type>text</type>
                <block>catalog/product_view_options_type_text</block>
                <template>demac/customoptionsregex/catalog/product/view/options/type/text.phtml</template>
            </action>
         </reference>

    </catalog_product_view>

</layout>

(this again is a copy of the default template with a few adjustments noted with the inline comments)
app/design/frontend/base/default/template/demac/customoptionsregex/catalog/product/view/options/type/text.phtml

<?php $_option = $this->getOption(); ?>
<dt><label<?php if ($_option->getIsRequire()) echo ' class="required"' ?>><?php if ($_option->getIsRequire()) echo '<em>*</em>' ?><?php echo  $this->htmlEscape($_option->getTitle()) ?></label>
    <?php echo $this->getFormatedPrice() ?></dt>
<dd<?php if ($_option->decoratedIsLast){?> class="last"<?php }?>>
    <div class="input-box">
    <?php if ($_option->getType() == Mage_Catalog_Model_Product_Option::OPTION_TYPE_FIELD): ?>
        <?php //Add custom regex validation class if required ?>
        <input type="text" onchange="opConfig.reloadPrice()" id="options_<?php echo $_option->getId() ?>_text" class="<?php echo $_option->getRegex()?'validate-regex-'.$_option->getId():''?> input-text<?php echo $_option->getIsRequire() ? ' required-entry' : '' ?> <?php echo $_option->getMaxCharacters() ? ' validate-length maximum-length-'.$_option->getMaxCharacters() : '' ?> product-custom-option" name="options[<?php echo $_option->getId() ?>]" value="<?php echo $this->escapeHtml($this->getDefaultValue()) ?>" />
    <?php elseif ($_option->getType() == Mage_Catalog_Model_Product_Option::OPTION_TYPE_AREA): ?>
        <?php //Add custom regex validation class if required ?>
        <textarea id="options_<?php echo $_option->getId() ?>_text" onchange="opConfig.reloadPrice()" class="<?php echo $_option->getRegex()?'validate-regex-'.$_option->getId():''?> <?php echo $_option->getIsRequire() ? ' required-entry' : '' ?> <?php echo $_option->getMaxCharacters() ? ' validate-length maximum-length-'.$_option->getMaxCharacters() : '' ?> product-custom-option" name="options[<?php echo $_option->getId() ?>]" rows="5" cols="25"><?php echo $this->escapeHtml($this->getDefaultValue()) ?></textarea>
    <?php endif; ?>
    <?php if ($_option->getMaxCharacters()): ?>
        <p class="note"><?php echo Mage::helper('catalog')->__('Maximum number of characters:')?> <strong><?php echo $_option->getMaxCharacters() ?></strong></p>
    <?php endif; ?>
    </div>
</dd>

<?php if($_option->getRegex()): // Add a unique validator for each new input requiring regex validation?>	
    <script>
        Validation.add('validate-regex-<?php echo $_option->getId()?>','<?php echo $_option->getRegexMessage()?$_option->getRegexMessage():$this->__('Invalid Input')?>',function(the_field_value){
            var re = new RegExp(<?php echo $_option->getRegex() ?>);
            return re.test(the_field_value);
        });
    </script>
<?php endif; ?>

That’s it, with the above code you should be able to add regex validation to your next project and to make it even easier here is a link to a ready-to-paste zip of the code above Demac_CustomOptionsRegex.zip.