A Namespaced registry (I was bored)

Coding Critique is the place to post source code for peer review by other members of DevNetwork. Any kind of code can be posted. Code posted does not have to be limited to PHP. All members are invited to contribute constructive criticism with the goal of improving the code. Posted code should include some background information about it and what areas you specifically would like help with.

Popular code excerpts may be moved to "Code Snippets" by the moderators.

Moderator: General Moderators

Post Reply
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

A Namespaced registry (I was bored)

Post by Chris Corbyn »

When I get in to the office in the morning I usually make a cup of coffee, then another coffee and write some non-work-related code to get into the swing of things :P

I wrote this today.... relatively pointless and "obvious" but here it is all the same. It's just a registry that can contain sub-registries, which in turn can contain sub-registries and so-on making a tree. It does provide a clean way to namespace your registry though.

Code: Select all

<?php

/**
 * A simple registry with a tree structure for namespacing
 * Just the result of some procrastinating at work 
 * @author d11wtq - Chris Corbyn
 */
class TreeRegistry
{
	/**
	 * A singleton instance, purely for convenience
	 * @static
	 * @var TreeRegistry
	 */
	protected static $instance = null;
	/**
	 * Registered objects
	 * @var array
	 */
	protected $entries = array();
	/**
	 * Children (branches)
	 * @var array TreeRegistry
	 */
	protected $children = array();
	
	/**
	 * Create and/or fetch a singleton
	 * @static
	 * @return TreeRegistry
	 */
	public static function getInstance()
	{
		if (self::$instance === null) { self::$instance = new TreeRegistry(); }
		return self::$instance;
	}
	/**
	 * Register a new child branch (namespace)
	 * @param string The name of the branch
	 */
	public function createChild($name)
	{
		$this->children[$name] = new TreeRegistry();
	}
	/**
	 * Unregister a child branch
	 * @param string The name of the branch
	 */
	public function removeChild($name)
	{
		if (isset($this->children[$name])) { unset($this->children[$name]); }
	}
	/**
	 * Magic Method to get the instance of the child
	 * @param string The name of the requested property
	 * @return TreeRegistry
	 */
	public function __get($property)
	{
		if (isset($this->children[$property]))
		{
			return $this->children[$property];
		}
		else
		{
			return null;
		}
	}
	/**
	 * Register an object in the registry
	 * @param Object The object being registered
	 * @param string A key to identify the object by
	 */
	public function register($object, $key)
	{
		$this->entries[$key] = $object;
	}
	/**
	 * Check if an object has been registered
	 * @param string The key used to identify the object
	 */
	public function has($key)
	{
		return isset($this->entries[$key]);
	}
	/**
	 * Get an object back out of the registry
	 * @param string The key used to register the object
	 * @return Object
	 * @throws Exception If the item is not registered
	 */
	public function get($key)
	{
		if ($this->has($key))
		{
			return $this->entries[$key];
		}
		else
		{
			throw new Exception("No such item [" . $key . "] is registered here");
		}
	}
	/**
	 * Unregister a registered object
	 * @param string The key used to identify the object
	 */
	public function unregister($key)
	{
		if ($this->has($key)) { unset($this->entries[$key]); }
	}
}
Testcase

Code: Select all

<?php

require_once "/home/d11wtq/simpletest/unit_tester.php";
require_once "/home/d11wtq/simpletest/reporter.php";
require_once "/home/d11wtq/simpletest/mock_objects.php";
require_once "./TreeRegistry.php";

class Nothing {}
Mock::Generate("Nothing");

class TestOfTreeRegistry extends UnitTestCase
{
	public function testRegisteredObjectsAreReturnedByReference()
	{
		$mock1 = new MockNothing();
		$mock2 = new MockNothing();
		$registry = new TreeRegistry();
		$registry->register($mock1, "one");
		$registry->register($mock2, "two");
		$this->assertReference($mock1, $registry->get("one"));
		$this->assertReference($mock2, $registry->get("two"));
	}
	
	public function testRegistryCanSeeIfObjectsAreRegistered()
	{
		$mock1 = new MockNothing();
		$mock2 = new MockNothing();
		$registry = new TreeRegistry();
		$this->assertFalse($registry->has("one"));
		$registry->register($mock1, "one");
		$this->assertTrue($registry->has("one"));
		$this->assertFalse($registry->has("two"));
		$registry->register($mock2, "two");
		$this->assertTrue($registry->has("two"));
	}
	
	public function testRegisteredObjectsCanBeUnregistered()
	{
		$mock1 = new MockNothing();
		$mock2 = new MockNothing();
		$registry = new TreeRegistry();
		$registry->register($mock1, "one");
		$this->assertTrue($registry->has("one"));
		$registry->register($mock2, "two");
		$this->assertTrue($registry->has("two"));
		$registry->unregister("one");
		$this->assertFalse($registry->has("one"));
		$registry->unregister("two");
		$this->assertFalse($registry->has("two"));
	}
	
	public function testExceptionIsThrownIfUnregisteredObjectIsRequested()
	{
		$registry = new TreeRegistry();
		$mock1 = new MockNothing();
		$registry->register($mock1, "one");
		try {
			$o = $registry->get("one");
			$this->pass();
		} catch (Exception $e) {
			$this->fail("No exception should be thrown since 'one' is registered");
		}
		
		try {
			$o = $registry->get("two");
			$this->fail("An exception should have been thrown since 'two' is not registered");
		} catch (Exception $e) {
			$this->pass();
		}
	}
	
	public function testChildrenCanBeSpawned()
	{
		$registry = new TreeRegistry();
		$registry->createChild("foo");
		$this->assertIsA($registry->foo, "TreeRegistry");
		$registry->createChild("bar");
		$this->assertIsA($registry->bar, "TreeRegistry");
	}
	
	public function testBranchesCanBeRemovedOnceCreated()
	{
		$registry = new TreeRegistry();
		$this->assertFalse($registry->foo);
		$registry->createChild("foo");
		$this->assertTrue($registry->foo);
		$registry->removeChild("foo");
		$this->assertFalse($registry->foo);
	}
	
	public function testSingletonFactoryReturnsASingleton()
	{
		$registry1 = TreeRegistry::getInstance();
		$registry2 = TreeRegistry::getInstance();
		$this->assertReference($registry1, $registry2);
	}
}

$test = new TestOfTreeRegistry();
$test->run(new HtmlReporter());
Example

Code: Select all

class Foo {}
class Bar {}

$registry = TreeRegistry::getInstance();
$registry->register(new Foo(), "foo");
$registry->register(new Bar(), "bar");

//Make a namespace?
$registry->createChild("ns1");

$registry->ns1->register(new Foo(), "foo");
$registry->ns2->register(new Bar(), "bar");

//and another?
$registry->ns1->createChild("ns2");

$registry->ns1->ns2->register(new Foo(), "foo");

var_dump($registry->ns1->get("foo"));
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post by Begby »

This is pretty cool, one thing you might want to consider is using the composite/component patter on it where you have a tree interface and 2 concrete objects that implement the interface. One would be a leaf, another would represent the branch.

Next, with the above setup, you could have your tree implement IteratorAggregate. That way you could iterate through all the leaves in either the root, at one level, or using a RecursiveIteratorIterator loop through all the leaves through the entire tree like

Code: Select all

foreach($tree as $leaf)
{
  // Get the root leaves
}

foreach(new RecursiveIteratorIterator($tree->getIterator()) as $leaf )
{
  echo $leaf->getPath() ; // This would loop through every leaf in the entire tree
}

Yet more ideas.....

Not only that but you could implement Countable and have return the count of all the leaf objects at a certain level like

Code: Select all

count($tree) ;  // all the elements in the tree
count($tree->ns1) ; // all the elements under ns1
But wait! There is more. You could also implement arrayaccess to force separation between leaves and branches and to replace your register method.

Code: Select all

$tree->ns1['taco'] // retrieve a leaf
$tree->ns1->ns2 // Retrieve a branch
$tree->ns1->taco // Fail since taco doesn't exist as a branch
$tree->ns1['leafName'] = new SomeObject() // This replaces your register() method to add a leaf
unset($tree->ns1->ns2['leaf']) ;
Or, instead of the above, you could also make __set() to replace your register() method and skip arrayaccess and then implement __isset() and __unset().

Code: Select all

$tree->ns1->leaf = new SomeObject() ;
isset($tree->ns1->leaf) ;
unset($tree->ns1->leaf) ;
So yeah, there are a ton more things you could do. You could end up with a registry that not only functioned like an associative array, but was also easy to iterate through recursively, but would also be able to have custom methods at each node and leaf level.

I have created stuff like this for both a database mapper and a form mapper in the past. Its a lot of fun to code.
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

Composite Pattern would be good - simple enough to add...

Code: Select all

self::$instance = new TreeRegistry();
Why not:

Code: Select all

self::$instance = new self();
...less class names to edit when renaming/subclassing.
Post Reply