Practical PHP Patterns: Registry
Join the DZone community and get the full member experience.
Join For FreeA well known object that other ones can use to find related objects or service.
This vague definition leaves open the possibility of abuse.
Implementation
The idea of a Registry is simple: providing a dynamic possibility for discovering collaborator objects, so that we not hardcode static calls to global objects like Singletons in each of them. In the testing environment, we can fill the Registry with mocks.
However, the problem is that then we hardcode static calls to the Registry, which is an effective sinkhole for dependencies in the case of a general purpose implementation. You can't limit which objects of a Registry are accessed by a client, so depending on the Registry may mean depending on each stored component.
In this scenario, the Registry becomes a Service Locator, the poor man's version of Dependency Injection.
Fixing it
My suggestion before implementing this pattern is thinking about how many of the registered objects would the ordinary client object actually need; maybe you can inject them directly.
A constraint I'd like to set for Registry implementations is that all the contained objects should be of the same type (or of a Layer Supertype). With this limitation, incarnations of the Registry become very useful as they can be injected and passed around as a virtual collection of object, even if they do not exist/currently are in persistence/are not all in memory at the same time.
If this reminds you of the Repository, you're right; the Repository pattern is a specialization of the Registry one, with this limitation written with blood in its contract in a night of full moon.
This is what a Registry is supposed to be: not an hidden blackboard like Zend_Registry where you can practice accumulate and fire, and setup time-ticking bombs that will explode later.
Here's an example from our test suite, which used Zend_Registry to set up the locale. One day, uncommenting a test would make another one fail (it used Zend_Registry, and the uncommented one messed with some keys). Fixing it takes some minutes (but it should have taken seconds). Scale that to a test suite with one thousands tests and you can easily destroy your tests isolation. Shortly, one test would pass when executed alone, but explode when the whole test suite (which maybe takes 15 minutes to run) because a previous test, whose identity you do not know, modified some global state. Good luck fixing that and grepping for 'Zend_Registry::'.
Instead, a Registry can be a first-class citizen, an object that can be injected, by itself or hidden behind an interface of a composer, into any client object that sincerely needs access to the whole encapsulated collection.
Why you now feel a generic Registry could be handy
Lifecycle problems can create the need for a generic Registry. How do you access an object A from a client object B if A do not exist when B is created? Use a Registry. Of course, this solution does not go a long way: you're never sure that A would be created in time, and you should revise your design to discover why a long-lived object should hold a reference to a short-lived one.
In fact, higher-level or injected Factories ara a similar, but more powerful solution. If B needs A, and A has the same lifecycle, simply inject it via a request-wide Factory. If B and A have a shorter lifecycle than the one of the request (for example they are view helpers that may not be used at all in certain requests, so they are instantiated only when needed in the presentation layer), provide a Factory that craates both and inject the Factory in the right scope.
However, never inject a generic registry: it is a false solution. The Registry becomes a Service Locator, which is depended on by every piece of your application, and depends again on every piece of it. This is a benignuos form of what I call "hiding dependencies under the carpet", which can be solved by injecting the direct dependency instead of a provider.
Examples
Our code sample is the infamous Zend_Registry component from Zend Framework 1. It is a Singleton, and when testing controllers it is a typical resource that the testing harness must remember to reset between each test.
My comments are in italic.
<?php /** * Generic storage class helps to manage global data. * Here the error is "global". No data should be really global: * at most request-wide. * * @category Zend * @package Zend_Registry * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ class Zend_Registry extends ArrayObject { /** * Class name of the singleton registry object. * @var string */ private static $_registryClassName = 'Zend_Registry'; /** * Registry object provides storage for shared objects. * @var Zend_Registry */ private static $_registry = null; /** * Retrieves the default registry instance. * A Singleton is a recipe for hiding dependencies to a class. * * @return Zend_Registry */ public static function getInstance() { if (self::$_registry === null) { self::init(); } return self::$_registry; } /** * Initialize the default registry instance. * * @return void */ protected static function init() { self::setInstance(new self::$_registryClassName()); } /** * getter method, basically same as offsetGet(). * * This method can be called from an object of type Zend_Registry, or it * can be called statically. In the latter case, it uses the default * static instance stored in the class. * * @param string $index - get the value associated with $index * @return mixed * @throws Zend_Exception if no entry is registerd for $index. */ public static function get($index) { $instance = self::getInstance(); if (!$instance->offsetExists($index)) { require_once 'Zend/Exception.php'; throw new Zend_Exception("No entry is registered for key '$index'"); } return $instance->offsetGet($index); } /** * setter method, basically same as offsetSet(). * * This method can be called from an object of type Zend_Registry, or it * can be called statically. In the latter case, it uses the default * static instance stored in the class. * * @param string $index The location in the ArrayObject in which to store * the value. * @param mixed $value The object to store in the ArrayObject. * @return void */ public static function set($index, $value) { $instance = self::getInstance(); $instance->offsetSet($index, $value); } /** * Returns TRUE if the $index is a named value in the registry, * or FALSE if $index was not found in the registry. * * @param string $index * @return boolean */ public static function isRegistered($index) { if (self::$_registry === null) { return false; } return self::$_registry->offsetExists($index); } /** * Constructs a parent ArrayObject with default * ARRAY_AS_PROPS to allow acces as an object * * @param array $array data array * @param integer $flags ArrayObject flags */ public function __construct($array = array(), $flags = parent::ARRAY_AS_PROPS) { parent::__construct($array, $flags); } }
Opinions expressed by DZone contributors are their own.
Comments