Managing “soft” relations between PHP objects

March 28th, 2009 | by David |

Sometimes you may stumble upon the problem that you have to find related objects to other objects without actually knowing anything about the objects but their class names. In this case the best approach is to have a universal object resolver based on the fact that the objects themselves usually know best what relations they have. I want to show you a quite simple approach that I use myself in a symfony project.

The interface

First we have to make sure that each class you want to find relations for supports the requests involved. In OO programming this is usually done using an interface definition: I placed it in sfObjectResolverInterface.class.php

1
2
3
4
5
6
7
8
9
10
11
<?php
 
/**
 * Objects implementing this interface can be asked for relations
 * to other objects by the sfObjectResolver.
 */
interface sfObjectResolverInterface
{
  public function getUniqueId();
  public function findObject($className);
}

The object

There are only 2 required methods: getUniqueId() and findObject(). The first one is only needed for caching the resolver results as you will see later. The findObject($className) has to be implemented in a way that it returns an object of the given $className type. An implementation may look like this:

<?php
 
class Article implements sfObjectResolverInterface {
  [... lots of other stuff ...]
 
  public function getUniqueId()
  {
    return $this->getId();
  }
 
  public function findObject($className)
  {
    switch ($className)
    {
      case 'Author':
        return $this->getAuthor();
 
      case 'Comment':
        return $this->getLastComment();
 
      default:
        return null;
    }
  }
}

As you can see there is no magic going on here. For every object you want this to work you just create a list of supported class names and what to return in each case. Of course you could put anything in there you want to!

The resolver

Now let’s take a look at the resolver that is actually doing the work: sfObjectResolver.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
 
/**
 * Finds related objects and caches the relations. This is used for
 * relations of objects that require dynamic assignments or should
 * not be hard-coded.
 */
class sfObjectResolver
{
  private static $resolver = array();
 
  /**
   * Find the matching object for a given object and a class name.
   * Resolve calls are cached to improve performance.
   * 
   * To enable resolving on an object class the object must implement
   * the sfObjectResolverInterface.
   * 
   * @param Object $object Object to resolve class for
   * @param string $className Class name to resolve
   * 
   * @return Object Matching object or null
   */
  public static function resolve($object, $className)
  {
    if ($object instanceof $className)
    {
      return $object;
    }
    if (is_null($object) || !($object instanceof sfObjectResolverInterface))
    {
      return null;
    }
 
    $objectIdent = get_class($object).$object->getUniqueId();
 
    if (isset(self::$resolver[$objectIdent][$className]))
    {
      return self::$resolver[$objectIdent][$className];
    }
 
    self::$resolver[$objectIdent][$className] = $object->findObject($className);
 
    return self::$resolver[$objectIdent][$className];
  }
}

As you can see the resolver is called by calling sfObjectResolver::resolve($object, $className) and passing 2 parameters: the object you already have and the class name of the object you need. Then some basic tests follow:

26
27
28
29
30
31
32
33
    if ($object instanceof $className)
    {
      return $object;
    }
    if (is_null($object) || !($object instanceof sfObjectResolverInterface))
    {
      return null;
    }

First the resolver checks if the object already has the correct type (this makes the code more universal and reduces custom checks in some cases). If it is not, the resolver tests if the object is resolveable by checking for the interface.

If the object passes these tests, the caching key is generated and the resolver looks into its cache if a relation for this object has already been asked for:

35
36
37
38
39
40
    $objectIdent = get_class($object).$object->getUniqueId();
 
    if (isset(self::$resolver[$objectIdent][$className]))
    {
      return self::$resolver[$objectIdent][$className];
    }

This is of course no cache that saves anything permanently, just a value holder to reduce the work if the same relation is checked several times during one PHP call.

If there is nothing in the resolver cache, the actual resolving is done – again, there is no magic in here:

42
43
44
    self::$resolver[$objectIdent][$className] = $object->findObject($className);
 
    return self::$resolver[$objectIdent][$className];

The resolver just calls the object’s findObject() method, writes the result to the cache and returns it.

Summary

As you can see this is intended for “maybe” relations: there may be some related object or there may be none (in that case the resolver returns null). It is a very useful utility though as you will see in an upcoming article describing a flexible menu system for symfony.

In our example the resolver might be used like this to find the Author object for an Article object:

  // find an Author object for the given Article object
  $author = sfObjectResolver::resolve($article, 'Author');
 
  // if it's called again the cached result will be used
  $author = sfObjectResolver::resolve($article, 'Author');

Have fun!

  1. 5 Responses to “Managing “soft” relations between PHP objects”

  2. By Ryan Weaver on Mar 29, 2009 | Reply

    Interesting – a bit outside of the box for me – but I really like it. I wonder where you learned this from and exactly how you use it inside Symfony? I understand your example use at the very end, but in Symfony (assuming author and article are Propel/Doctrine objects), this relationship isn’t “soft” – it’s quite straightforward. Where exactly do you find that this method really comes in handy?

  3. By Nicolas Martin on Mar 29, 2009 | Reply

    Nice !

    Is your solution related with the ‘Registry’ design pattern ?

    I don’t know much about this pattern but it sounds pretty close to it.

  4. By David on Mar 30, 2009 | Reply

    @Ryan: I will provide an actual example in (hopefully) a few days – I already use the code, but it’s not nice enough yet for publication ;-)

    @Nicolas: As far as I can tell I see no direct connection to the “Registry” pattern. I haven’t checked though if there already is an “official” pattern describing what I did here.

  5. By David on Mar 30, 2009 | Reply

    Some further research showed that you could see the ObjectResolver as something similar to a “dictionary” pattern. What it does in the end is nothing more than looking up objects for given keys. The interface to ask other objects for their dictionary entries is only 1 possibility to use this.

    Of course you could also losen the strict “className” parameter and make it some universal key instead – the changes to the code would be minimal (removing lines 26 to 29 of sfObjectResolver.class.php). Until I have another need for it I think I will keep it as it is though – at the moment it’s perfect for my needs.

  6. By Les on Aug 6, 2009 | Reply

    > I don’t know much about this pattern but it sounds pretty close to it.

    In simpler terms, yes as the Registry is just your basic container and little else; what this article speculates about is more akin to a Service Locator but in which case why bother?

    Go the whole hog and go with a Dependency Injection solution instead which would offer you far more flexibility abeit slightly more complexity…

    Could your brain cope with that?

Post a Comment