SunShop Modifications Part 4

Here in Part 4 I show another technique which I use to keep my custom code off to the side. I’m no longer a fan of the Singleton Design Pattern because of its difficulty in unit testing. However, it’s a good tool for SunShop modifications.

Our Top Sellers page has two tabs, one tab for All time top selling products, and another tab for top sellers over the past 30 days. I use “bb_” as my prefix to ensure no namespace collisions. Here is the page template:

<!-- Begin list_topseller.html -->
<h2><img src="themes/$settings[theme]/images/gray_h_arrow.gif" border="0" alt="" class="left_float_3_px" />Top Sellers</h2>
 <div class="tabber">
    <div class="tabbertab">
        <h2>Best Sellers Last 30 Days</h2>
        <div class="content">
           <table cellpadding="2" cellspacing="2">
<!-- Plugin bb_topseller_30days -->
$plugin[bb_topseller_30days]
           </table>
        </div>
    </div>
    <div class="tabbertab">
        <h2>All Time Best Sellers</h2>
        <div class="content">
           <table cellpadding="2" cellspacing="2">
<!-- Plugin bb_topseller_alltime -->
$plugin[bb_topseller_alltime]
           </table>
        </div>
    </div>
 </div>
<div class="clear"></div>
<!-- End list_topseller.html -->

I described the plugin mechanism in Part 1. My calculations are done with the two plugin calls $plugin[bb_topseller_30days] and $plugin[bb_topseller_alltime]. Note the html markers at the top and bottom of the theme and before each plugin call. That allows me to easily find and verify where I am in the page source. When I come back in six months to change something, a quick check of the page source will point me to the write source code.

I purposely did NOT indent lines 8-9 and 17-18. This allows me to quickly pick out the plugin-output areas when viewing page source.

Plugins are Invariant

It would be nice to write a single plugin, and pass it whether I want to generate “all time” output or “30 day” output. That’s not available in the plugin mechanism. However, what I can do is create an object registry. I can use the same object once for the “30 days” calculation, and reuse the object for the “all time” calculation. I’d like to cache any relatively-expensive operation.

Object Registry

Here is my object registry:

/**
 * Ed Barnard, 26 January 2013
 *
 * This singleton class allows plugin development "off to the
 * side" without interfering with the SunShop code. This class
 * is a combination of Singleton, Registry, Factory Method
 * design patterns.
 *
 * The general motivation is to cache results without modifying
 * the mainline SunShop code, so that we protect ourselves from
 * SunShop software updates. As we load and execute helper
 * objects, we keep them here in the registry. This way multiple
 * plugins can access the same cached results.
 *
 * A SunShop plugin is, in effect, a substitution variable. For
 * example, we can place:
 *   $plugin[debug]
 * on any template page, and the output of that plugin's
 * render() method will be placed there. Plugin markers accept
 * no parameters, so therefore if we have three slight
 * variations on a page, we need three plugins. When all three
 * plugins require relatively expensive computations, we would
 * need to do the computation three times.
 *
 * That is the motivation for this class. All three plugins can
 * access the same helper object through this registry, and the
 * computation only needs to be done once (with the helper
 * object having methods for producing the three variations).
 *
 * Each registry-compatible object should have _construct() and
 * initialize() methods which get called once upon
 * instantiation. The remaining methods would be
 * plugin-specific.
 *
 * Note that each plugin is itself a skeleton which includes and
 * instantiates a new instance of its helper object. It is the
 * plugin's helper object which can make use of these
 * registry-compatible objects.
 *
 * From a testing standpoint, the Singleton Registry is an
 * anti-pattern. However, it is the best solution available for
 * protecting custom code from SunShop updates.
 */

/**
 * Load other globally-expected custom classes here, so that
 * only one 'require' needs to be inserted in index.php and
 * admin/adminindex.php (i.e., the index files need to load this
 * registry class).
 */

class BBRegistry {
    private $objects = array();
    private static $instance;
    private $includepath;
    /* Name the registry-compatible interface */
    private static $registrycompatible = 'BBRegistryCompatible';
    private static $requiredIncludes = array(
        'interface.bb.registrycompatible.php',
        'class.bb.registrycompatible.base.php',
        );

    /**
     * Custom code can potentially be placed outside the SunShop
     * classes area. So long as the RegistryCompatible classes and
     * interfaces are in the same folder as this file, we can find
     * them.
     *
     * @author Ed Barnard (1/26/2013)
     */
    private function __construct() {
        $this->includepath = dirname(__FILE__);
        $this->loadRequiredIncludes();
    }

    private function loadRequiredIncludes() {
        foreach(self::$requiredIncludes as $file) {
            $this->customInclude($file);
        }
    }

    public static function getInstance() {
        if(empty(self::$instance)) {
            self::$instance = new self;
        }
        return self::$instance;
    }

    public static function getRegisteredObject($class, $file) {
        $registry = self::getInstance();
        return $registry->instantiateOnce($class, $file);
    }

    private function instantiateOnce($class, $file) {
        if(!array_key_exists($class, $this->objects)) {
            $this->objects[$class] = null;
            $this->customInclude($file);
            if(class_exists($class, false) && $this->isRegistryCompatible($class)) {
                $object = new $class();
                $object->initialize();
                $this->objects[$class] = $object;
            }
        }
        return $this->objects[$class];
    }

    private function isRegistryCompatible($class) {
        $implements = class_implements($class, $false);
        return in_array(self::$registrycompatible, $implements);
    }

    public function customInclude($file) {
        @include_once($this->includepath.'/'.$file);
    }

}

It looks like I actually wrote some documentation for this one. How convenient! The object registry BBRegistry is a singleton. Get its instance with BBRegistry::getInstance().

This is a relatively large amount of code. What is its purpose? See lines 89-92. The whole point of this registry is the BBRegistry::getRegisteredObject($class, $file) static call. Anything which conforms to our Registered Object interface (see below) can be invoked by the getRegisteredObject call. In other words, so long as I can construct any part of my business logic as a helper object, I can get to it via getRegisteredObject. The object will persist throughout the life of the page load.

In our case, we need to do the “top seller” calculations once, and generate output twice. Thus the object’s persistence has value to us.

Here is the Registered Object interface specification:

interface BBRegistryCompatible {
    public function __construct();
    public function initialize();
}

Registered Object

I arbitrarily decided that every Registered Object is to be a subclass of this abstract base class:

abstract class BBRegistryCompatibleBase implements BBRegistryCompatible {
}

Why bother with the above? This gains me:

  • Should I ever find common code, it can be refactored and placed in the abstract base class above.
  • By declaring the base class to meet the BBRegistryCompatible interface specification, I am forcing all subclasses to meet the specification whether or not I remembered to declare its interface. Also, I have refactored the common code (i.e., the “implements BBRegistryCompatible”) into the base class.
  • Each Registered Object automatically documents that it IS a registered object because each Registered Object declares itself as extending BBRegistryCompatibleBase.
  • This abstract base class forces me to implement the entire interface (__construct() and initialize()) in each Registered Object. Why? Because I have declared this class to be abstract, and have declared this class to implement BBRegistryCompatible, the PHP compiler will enforce the requirements in each Registered Object.

What’s Next?

In part 5, we will look at the Top Seller plugins, and see how we place the business logic in a Registered Object for their use.