Lithium - Diving into the core, Adaptable

Bringing us to another installment of “Lithium – Diving into the core” is the Adaptable object. Adaptable gives us the the ability to provide an interaction interface without hard wiring the concrete implementation to perform said interaction.

While this may be a bit long winded, it’s really worth it as Adaptable is one of the most useful classes in the entire library. So grab some coffee, maybe pop open the repository to follow along and kick your feet up.

A great example of the Adaptable object is the Session object. A session, at it’s most basic, is a means to persist information about the current user across requests. As such it implements a read and write method, along with some others, to do its job. The beauty of Adaptable is that it actually allows the class to leave the heavy lifting to its adapters; in this case Cookie, Memory or Php

Implementation

Comparing the Adaptable and Session object you’ll notice that it’s the job of the adapter to actually implement the interface it will end up providing. For Session this is pretty straight forward: read, write, delete, clear and check — all things that make sense when working with a session.

When creating your own Adaptable subclass you’ll need to determine the methods that make sense when dealing with the problem in question. Here’s a couple examples off the top of my head to help illustrate this point:

  • Bank deposit, charge and balance — Credit card, debit card or check, these are all applicable actions.
  • Music Player play, stop, status, songs — iTunes, Winamp, VLC, etc. These are universally recognized actions.
  • SocialSite getPosts, addPost, message — Facebook, Twitter anyone?

The thing to take away from this is that the adapter must be able to implement all of the methods for the class that uses it. The goal is to let someone swap adapters at anytime and not to have the code notice anything different.

Configuration

Continuing with the Session object lets take a look at how it’s configured in app/config/bootstrap/session.php

Session::config(array(
    'cookie' => array('adapter' => 'Cookie', 'name' => $name),
    'default' => array('adapter' => 'Php', 'session.name' => $name)
));

Here we’re defining two configurations: cookie and default. One uses the Cookie adapter and the other Php. This tells the Session object which adapter to utilize when we use a specific configuration.

You’ll notice that there is some extra, specific configuration bits. This is normally ignored by the Session object (aside from “strategies”, more on that in a bit) and instead used to help configure the specific adapter.

Why would you need to pass options to an adapter? Well when writing your own adapters you may require certain things: API keys, URLs, API version, etc. The configuration is the place for those. It is not, however, the place to store data that should otherwise be passed in as an option when calling a method in the adapter.

Anatomy of a request

Alright, now lets get our hands dirty. To do the Adaptable object justice we’ll need to look at an actual method in the Session object. Bear with me, here.

public static function read($key = null, array $options = array()) {
    $defaults = array('name' => null, 'strategies' => true);
    $options += $defaults;
    $method = ($name = $options['name']) ? static::adapter($name)->read($key, $options) : null;
    $settings = static::_config($name);

    if (!$method) {
        foreach (array_keys(static::$_configurations) as $name) {
            if ($method = static::adapter($name)->read($key, $options)) {
                break;
            }
        }
        if (!$method || !$name) {
            return null;
        }
    }
    $filters = $settings['filters'] ?: array();
    $result = static::_filter(__FUNCTION__, compact('key', 'options'), $method, $filters);

    if ($options['strategies']) {
        $options += array('key' => $key, 'mode' => 'LIFO', 'class' => __CLASS__);
        return static::applyStrategies(__FUNCTION__, $name, $result, $options);
    }
    return $result;
}

(I’m going to gloss over certain areas that aren’t fundamentally important to this discussion — I recommend you open and read the Session.php file to dig in further)

Now if you didn’t look closely you may have missed it. The key here is:

$method = ($name = $options['name']) ? static::adapter($name)->read($key, $options) : null;

If you’ve told it which configuration to use it will call the read method on that adapter. The if block below it is triggered if you don’t tell it which configuration to use so that it can iterate over all of its adapters and try the read method on each.

That’s it!

Since each adapter knows if it’s capable of responding and will if it can, the Session object just needs to loop over and ask each to read from its concrete implementation; Php, Memory, Cookie, whatever. How they actually get their information is immaterial to the Session object itself.

The static::_filter call is your run of the mill trigger for any filters that may be waiting to listen for or manipulate the data. A neat thing to point out is that if there are filters attached, they don’t need to be attached to the concrete implementation (Php::read for instance) but instead can filter the Session class directly.

Now, about those other bits…

Strategies

Strategies are, in a way, Lithium provided filters that can operate on an action (Session::read in this case) regardless of what the concrete implementation may be.

Similar to adapter objects they may have their own configuration and should implement the same methods that the “base” class does when it needs to operate. A perfect example of this is the Encrypt strategy.

Like our adapter it implements read and write but unlike them it doesn’t care about check, delete, or any of the others. This is because there really isn’t any encryption or decryption to be done when you just want to delete an entry. The strategy should implement all the methods it needs to perform its task.

On the other hand, the Hmac strategy, which maintains a signature to confirm the validity of its data does need to tack into delete since the signature will change when data is removed.

Both Encrypt and Hmac need to store extra data away to do their jobs. For instance Hmac needs to write away the signature for future requests to confirm there has been no tampering.

$class::write('__signature', $signature, array('strategies' => false) + $options);

This is the line from the Hmac strategy to store away the signature its compiled and you’ll notice that it uses the write method to tuck its signature away. Again, because the $class (which is Session in this case) merely delegates off to an adapter, Hmac doesn’t care who that may be. To make sure other strategies don’t interfere with its operation or to cause an infinite loop, it tells Session not to trigger any strategies when performing its write

A quick note to bring things full circle. From our little SocialSite example above, it’s pretty easy to think up some strategies that could be used for all implementations:

  • UrlShortener When posting a message find any URLs and shorten them with a service.
  • Shortener Attempt to shorten a message by replacing common words with acronyms.
  • LinkReplacer When reading messages, find anything that looks like a URL and wrap it up in the appropriate HTML.

(These are just meant as crude examples)

Notes

  • A strategy should, more or less, be completely transparent to the operation and removing or adding one should not stop the class from performing its duties.
  • An adapter must be able to perform all the methods available to the application from the abstract object (Session for example)
  • An adapter should be interchangeable without impacting the application that may be using it aside from possibly requiring certain configuration.

Closing

whew — Hopefully this gives you a better grasp of the Adaptable object and its children and their purpose in Lithium. Now, your homework is to go crack open the Session.php and Cache.php files to try and apply your new knowledge to better understand them.

Extra credit for those who go read the strategies.