PSR-6 Meta Document

PSR-Cache Meta Document

1. Summary

Caching is a common way to improve the performance of any project, making caching libraries one of the most common features of many frameworks and libraries. This has lead to a situation where many libraries roll their own caching libraries, with various levels of functionality. These differences are causing developers to have to learn multiple systems which may or may not provide the functionality they need. In addition, the developers of caching libraries themselves face a choice between only supporting a limited number of frameworks or creating a large number of adapter classes.

2. Why Bother?

A common interface for caching systems will solve these problems. Library and framework developers can count on the caching systems working the way they're expecting, while the developers of caching systems will only have to implement a single set of interfaces rather than a whole assortment of adapters.

Moreover, the implementation presented here is designed for future extensibility. It allows a variety of internally-different but API-compatible implementations and offers a clear path for future extension by later PSRs or by specific implementers.

Pros:

  • A standard interface for caching allows free-standing libraries to support caching of intermediary data without effort; they may simply (optionally) depend on this standard interface and leverage it without being concerned about implementation details.
  • Commonly developed caching libraries shared by multiple projects, even if they extend this interface, are likely to be more robust than a dozen separately developed implementations.

Cons:

  • Any interface standardization runs the risk of stifling future innovation as being "not the Way It's Done(tm)". However, we believe caching is a sufficiently commoditized problem space that the extension capability offered here mitigates any potential risk of stagnation.

3. Scope

3.1 Goals

  • A common interface for basic and intermediate-level caching needs.
  • A clear mechanism for extending the specification to support advanced features, both by future PSRs or by individual implementations. This mechanism must allow for multiple independent extensions without collision.

3.2 Non-Goals

  • Architectural compatibility with all existing cache implementations.
  • Advanced caching features such as namespacing or tagging that are used by a minority of users.

4. Approaches

4.1 Chosen Approach

This specification adopts a "repository model" or "data mapper" model for caching rather than the more traditional "expire-able key-value" model. The primary reason is flexibility. A simple key/value model is much more difficult to extend.

The model here mandates the use of a CacheItem object, which represents a cache entry, and a Pool object, which is a given store of cached data. Items are retrieved from the pool, interacted with, and returned to it. While a bit more verbose at times it offers a good, robust, flexible approach to caching, especially in cases where caching is more involved than simply saving and retrieving a string.

Most method names were chosen based on common practice and method names in a survey of member projects and other popular non-member systems.

Pros:

  • Flexible and extensible
  • Allows a great deal of variation in implementation without violating the interface
  • Does not implicitly expose object constructors as a pseudo-interface.

Cons:

  • A bit more verbose than the naive approach

Examples:

Some common usage patterns are shown below. These are non-normative but should demonstrate the application of some design decisions.

/**
 * Gets a list of available widgets.
 *
 * In this case, we assume the widget list changes so rarely that we want
 * the list cached forever until an explicit clear.
 */
function get_widget_list()
{
    $pool = get_cache_pool('widgets');
    $item = $pool->getItem('widget_list');
    if (!$item->isHit()) {
        $value = compute_expensive_widget_list();
        $item->set($value);
        $pool->save($item);
    }
    return $item->get();
}
/**
 * Caches a list of available widgets.
 *
 * In this case, we assume a list of widgets has been computed and we want
 * to cache it, regardless of what may already be cached.
 */
function save_widget_list($list)
{
    $pool = get_cache_pool('widgets');
    $item = $pool->getItem('widget_list');
    $item->set($list);
    $pool->save($item);
}
/**
 * Clears the list of available widgets.
 *
 * In this case, we simply want to remove the widget list from the cache. We
 * don't care if it was set or not; the post condition is simply "no longer set".
 */
function clear_widget_list()
{
    $pool = get_cache_pool('widgets');
    $pool->deleteItems(['widget_list']);
}
/**
 * Clears all widget information.
 *
 * In this case, we want to empty the entire widget pool. There may be other
 * pools in the application that will be unaffected.
 */
function clear_widget_cache()
{
    $pool = get_cache_pool('widgets');
    $pool->clear();
}
/**
 * Load widgets.
 *
 * We want to get back a list of widgets, of which some are cached and some
 * are not. This of course assumes that loading from the cache is faster than
 * whatever the non-cached loading mechanism is.
 *
 * In this case, we assume widgets may change frequently so we only allow them
 * to be cached for an hour (3600 seconds). We also cache newly-loaded objects
 * back to the pool en masse.
 *
 * Note that a real implementation would probably also want a multi-load
 * operation for widgets, but that's irrelevant for this demonstration.
 */
function load_widgets(array $ids)
{
    $pool = get_cache_pool('widgets');
    $keys = array_map(function($id) { return 'widget.' . $id; }, $ids);
    $items = $pool->getItems($keys);

    $widgets = array();
    foreach ($items as $key => $item) {
        if ($item->isHit()) {
            $value = $item->get();
        } else {
            $value = expensive_widget_load($id);
            $item->set($value);
            $item->expiresAfter(3600);
            $pool->saveDeferred($item, true);
        }
        $widget[$value->id()] = $value;
    }
    $pool->commit(); // If no items were deferred this is a no-op.

    return $widgets;
}
/**
 * This examples reflects functionality that is NOT included in this
 * specification, but is shown as an example of how such functionality MIGHT
 * be added by extending implementations.
 */

interface TaggablePoolInterface extends Psr\Cache\CachePoolInterface
{
    /**
     * Clears only those items from the pool that have the specified tag.
     */
    clearByTag($tag);
}

interface TaggableItemInterface extends Psr\Cache\CacheItemInterface
{
    public function setTags(array $tags);
}

/**
 * Caches a widget with tags.
 */
function set_widget(TaggablePoolInterface $pool, Widget $widget)
{
    $key = 'widget.' . $widget->id();
    $item = $pool->getItem($key);

    $item->setTags($widget->tags());
    $item->set($widget);
    $pool->save($item);
}

4.2 Alternative: "Weak item" approach

A variety of earlier drafts took a simpler "key value with expiration" approach, also known as a "weak item" approach. In this model, the "Cache Item" object was really just a dumb array-with-methods object. Users would instantiate it directly, then pass it to a cache pool. While more familiar, that approach effectively prevented any meaningful extension of the Cache Item. It effectively made the Cache Item's constructor part of the implicit interface, and thus severely curtailed extensibility or the ability to have the cache item be where the intelligence lives.

In a poll conducted in June 2013, most participants showed a clear preference for the more robust if less conventional "Strong item" / repository approach, which was adopted as the way forward.

Pros:

  • More traditional approach.

Cons:

  • Less extensible or flexible.

4.3 Alternative: "Naked value" approach

Some of the earliest discussions of the Cache spec suggested skipping the Cache Item concept all together and just reading/writing raw values to be cached. While simpler, it was pointed out that made it impossible to tell the difference between a cache miss and whatever raw value was selected to represent a cache miss. That is, if a cache lookup returned NULL it's impossible to tell if there was no cached value or if NULL was the value that had been cached. (NULL is a legitimate value to cache in many cases.)

Most more robust caching implementations we reviewed -- in particular the Stash caching library and the home-grown cache system used by Drupal -- use some sort of structured object on get at least to avoid confusion between a miss and a sentinel value. Based on that prior experience FIG decided that a naked value on get was impossible.

4.4 Alternative: ArrayAccess Pool

There was a suggestion to make a Pool implement ArrayAccess, which would allow for cache get/set operations to use array syntax. That was rejected due to limited interest, limited flexibility of that approach (trivial get and set with default control information is all that's possible), and because it's trivial for a particular implementation to include as an add-on should it desire to do so.

5. People

5.1 Editor

  • Larry Garfield

5.2 Sponsors

  • Paul Dragoonis, PPI Framework (Coordinator)
  • Robert Hafner, Stash

6. Votes

Acceptance vote on the mailing list

7. Relevant Links

Note: Order descending chronologically.

8. Errata

8.1 Handling of incorrect DateTime values in expiresAt()

The CacheItemInterface::expiresAt() method's $expiration parameter is untyped in the interface, but in the docblock is specified as \DateTimeInterface. The intent is that either a \DateTime or \DateTimeImmutable object is allowed. However, \DateTimeInterface and \DateTimeImmutable were added in PHP 5.5, and the authors chose not to impose a hard syntactic requirement for PHP 5.5 on the specification.

Despite that, implementers MUST accept only \DateTimeInterface or compatible types (such as \DateTime and \DateTimeImmutable) as if the method was explicitly typed. (Note that the variance rules for a typed parameter may vary between language versions.)

Simulating a failed type check unfortunately varies between PHP versions and thus is not recommended. Instead, implementors SHOULD throw an instance of \Psr\Cache\InvalidArgumentException.
The following sample code is recommended in order to enforce the type check on the expiresAt() method:


class ExpiresAtInvalidParameterException implements Psr\Cache\InvalidArgumentException {}

// ...

if (! (
        null === $expiration
        || $expiration instanceof \DateTime
        || $expiration instanceof \DateTimeInterface
)) {
    throw new ExpiresAtInvalidParameterException(sprintf(
        'Argument 1 passed to %s::expiresAt() must be an instance of DateTime or DateTimeImmutable; %s given',
        get_class($this),
        is_object($expiration) ? get_class($expiration) : gettype($expiration)
    ));
}

8.2 Type additions

The 2.0 release of the psr/cache package includes scalar parameter types. The 3.0 release of the package includes return types. This structure leverages PHP 7.2 covariance support to allow for a gradual upgrade process, but requires PHP 8.0 for type compatibility.

The 2.0 version also corrects the Errata 8.1 above by providing a correct type hint for the CacheItemInterface::expiresAt() method's $expiration parameter. That results in a slight change in the error thrown on invalid input; as it is still a fatal disallowed case, FIG has deemed it an acceptably small BC break in order to leverage correct native typing.

Implementers MAY add return types to their own packages at their discretion, provided that:

  • the return types match those in the 3.0 package.
  • the implementation specifies a minimum PHP version of 8.0.0 or later.

Implementers MAY add parameter types to their own packages in a new major release, either at the same time as adding return types or in a subsequent release, provided that:

  • the parameter types match those in the 2.0 package.
  • the implementation specifies a minimum PHP version of 8.0.0 or later.
  • the implementation depends on "psr/cache": "^2.0 || ^3.0" so as to exclude the untyped 1.0 version.

Implementers are encouraged but not required to transition their packages toward the 3.0 version of the package at their earliest convenience.