Skip to content

Instantly share code, notes, and snippets.

@moisadoru
Last active December 14, 2015 12:29
Show Gist options
  • Save moisadoru/5087157 to your computer and use it in GitHub Desktop.
Save moisadoru/5087157 to your computer and use it in GitHub Desktop.
Some thoughts on caching in general, and a possible PSR Caching proposal

A. Caching systems in general:

  • there are many caching systems out there, with totally different APIs, levels of complexity, implementations and use-cases, so a minimum common API should provide some level of portability.
  • caching systems can work synchronously or asynchronously
  • usually caching systems store items for a limited amount of time (expiration, time-to-live, etc.)
  • some caching systems expose the expiration information in their API, others don't
  • those caching systems who use expiration may chose to use a predefined value, a system imposed value, or let the API consumer specify a value
  • the most basic operations are the storing and retrieval of things from the cache
  • caching systems don't always guarantee that an item is or will be stored when asked to
  • some caching systems function as a LRU (last recently used) system - which means that data that is not used often is evicted at some point
  • in fact, caching systems don't guarantee anything - they do their best to store a piece of information for a high speed delivery when needed
  • high speed delivery usually means that other aspects of data storage (integrity, availability) are sacrificed

B. Given these, the lowest possible level of interaction with a caching system would consist in just two methods:

<?php
    namespace Psr\Cache;

    interface Store
    {
        /**
         * Retrievs a named value from cache. Must return null if 
         * the value is expired or missing completely.
         *
         * @param string $key The name of the stored value to be retrieved.
         * @return mixed|null The stored value if found and valid, null otherwise.
         */
        public function retrieve($key);

        /**
         * Attempts to store a named value into cache.
         *
         * @param string $key   The name of the value to be stored.
         * @param mixed  $value The actual value to be stored. If null, any existing 
         *                      values for the same key will be removed or scheduled 
         *                      for removal ($ttl will be ignored in this case).
         * @param number $ttl   The amount of time we need the value stored. Ignored 
         *                      by systems that don't support TTL. A $ttl of value 0 
         *                      means no expiration. A value of -1 deletes the item.
         * @return bool         A success status. For asynchronous caches, will always 
         *                      return true if the value was successfully queued 
         *                      internally for storage.
         */
        public function store($key, $value = null, $ttl = 0);
    }

Note: we're assuming PHP 5.3.3+ (a method having the same name as a namespaced class will not be treated as constructors). For lower PHP versions, a method or interface rename would do the trick.

These two methods are the basic requirements for using a caching system.

A basic reference implementation (using the APC engine) could potentially be done like this:

<?php
    class ApcStoreRi implements \Psr\Cache\Store
    {
        /**
         * {@inheritdoc}
         */
        public function retrieve($key)
        {
            $success = false;
            $value   = apc_fetch($key, $success);
            return $success ? $value : null;
        }

        /**
         * {@inheritdoc}
         */
        public function store($key, $value = null, $ttl = 0)
        {
            return ($ttl < 0) ? apc_delete($key) : apc_store($key, $value, $ttl);
        }
    }

Note: The ApcStoreRi is just a potential implementation/example, and should be treated as such.

At times, special needs may arise:

  1. Manual invalidation/eviction:

A convenience delete($key) method could be added for such an operation (when the caching system explicitly exposes the invalidation functionality), but should not be required, so let's add it in a separate interface:

<?php
    namespace Psr\Cache;

    interface EvictableStore extends Store
    {
        /**
         * Attempts to remove a previously stored value.
         *
         * @param string $key   The name of the stored value to be evicted.
         * @return bool         A success status. For asynchronous caches, 
         *                      will always return true if the value was
         *                      successfully queued internally for eviction.
         */
        public function evict($key);
    }

A convenience abstraction could also be added:

<?php
    abstract class EvictableStoreRi implements \Psr\Cache\EvictableStore
    {
        /**
         * {@inheritdoc}
         */
        public function evict($key)
        {
            return $this->store($key, null, -1);
        }
    }

Our APC RI could potentially be done like this:

<?php
    class ApcStoreRi extends Psr\Cache\EvictableStoreRi
    {
        // ... insert the store() and retrieve() methods from the 
        // previous APC RI class here
    }

... or like this (if we keep the previous ApcStoreRi class):

<?php    
    class ApcEvictableStoreRi extends ApcStoreRi implements \Psr\Cache\EvictableStore
    {
        /**
         * {@inheritdoc}
         */
        public function evict($key)
        {
            return apc_delete($key);
        }
    }

Note: Normally, we would also implement the EvictableStore in the previously described ApcStoreRi directly. The modified ApcStoreRi and the ApcEvistableStoreRi are just potential implementations/examples, and should be treated as such.

  1. Checking for an item's existance.

A convenience contains($key) method could be added for such an operation, but should not be required, so let's add it in a separate interface:

<?php
    namespace Psr\Cache;

    interface QueryableStore extends Store
    {
        /**
         * Attempts to check if a value exists in the cache for a given name.
         *
         * @param string $key   The name of the stored value to be verified.
         * @return bool         Weather the value exists in the cache or not.
         */
        public function contains($key);
    }
A convenience abstraction could also be added:
<?php
    abstract class QueryableStoreRi implements QueryableStore
    {
        /**
         * {@inheritdoc}
         */
        public function contains($key)
        {
            return return (null !== $this->retrieve($key));
        }
    }
Our APC RI could potentially be done like this:
<?php
    class ApcStoreRi extends Psr\Cache\QueryableStoreRi
    {
        // ... insert the store() and retrieve() methods from 
        // the previous APC RI class here
    }

... or like this (if we keep the previous ApcStoreRi class):

<?php
    class ApcQueryableStoreRi extends ApcStoreRi implements Psr\Cache\QueryableStore
    {
        /**
         * {@inheritdoc}
         */
        public function contains($key)
        {
            return (bool) apc_exists($key);
        }
    }
  1. Bulk core (store and retrieve) operations:

Sometimes, bulk operations are desirable, so let's define an interface for multi key API calls:

<?php
    namespace Psr\Cache;

    interface MultiAccessStore extends Store
    {
        /**
         * Retrievs multiple named values from cache.
         *
         * @param array|Traversable $keys A list of value names to retrieve.
         * @return array                  A key/value map of stored value.
         */
        public function multiRetrieve($keys);

        /**
         * Attempts to store a named value into the cache.
         *
         * @param array|Traversable $values A key/values map of values to be stored.
         * @return array                    A key/success map.
         */
        public function multiStore($values);
    }

A convenience abstraction could also be added:

<?php
    abstract class MultiAccessStoreRi implements MultiAccessStore
    {
        /**
         * {@inheritdoc}
         */
        public function multiRetrieve($keys)
        {
            $results = array();
            if (is_array($keys) || ($keys instanceof \Traversable)) {
                foreach ($keys as $key) {
                    $results[$key] = $this->retrieve($key);
                }
            }
            return $results;
        }

        /**
         * {@inheritdoc}
         */
        public function multiStore($values)
        {
            $results = array();
            if (is_array($values) || ($values instanceof \Traversable)) {
                foreach ($values as $key => $value) {
                    if (is_array($value)) {
                        $params = array_merge(array($key), $value);
                    }
                    else {
                        $params = array($key, $value);
                    }
                    $callback = array($this, 'store');
                    $results[$key] = call_user_func_array($callback, $params);
                }
            }
            return $results;
        }
    }

Our APC RI could potentially be done like this:

<?php
    class ApcStoreRi extends Psr\Cache\MultiAccessStoreRi
    {
        // ... insert the store() and retrieve() methods from the 
        // previous APC RI class here
    }

... or like this (if we keep the previous ApcStoreRi class):

<?php
    class ApcQueryableStoreRi extends ApcStoreRi implements Psr\Cache\MultiAccessStoreRi
    {
        /**
         * {@inheritdoc}
         */
        public function multiRetrieve($keys)
        {
            return $this->retrieve($keys);
        }

        /**
         * {@inheritdoc}
         * Please note that TTL is not supported in this implementation.
         */
        public function multiStore($values)
        {
            return apc_store($values);
        }
    }

C. Conclusion.

While use-cases other than the above may arise, they could be added to the specification using the same mechanism as above.

Our final specification could look like this:

<?php
// The Contract
namespace Psr\Cache;
interface Store
{
/**
* Retrievs a named value from cache. Must return null if the value is expired or missing completely.
*
* @param string $key The name of the stored value to be retrieved.
* @return mixed|null The stored value if found and valid, null otherwise.
*/
public function retrieve($key);
/**
* Attempts to store a named value into cache.
*
* @param string $key The name of the value to be stored.
* @param mixed $value The actual value to be stored. If null, any existing values for the same key will be
* removed or scheduled for removal ($ttl will be ignored in this case).
* @param number $ttl The amount of time we need the value stored. Ignored by systems that don't support TTL.
* A $ttl of value 0 means no expiration. A value of -1 deletes the item.
* @return bool A success status. For asynchronous caches, will always return true if the value was
* successfully queued internally for storage.
*/
public function store($key, $value = null, $ttl = 0);
}
interface EvictableStore extends Store
{
/**
* Attempts to remove a previously stored value.
*
* @param string $key The name of the stored value to be evicted.
* @return bool A success status. For asynchronous caches, will always return true if the value was
* successfully queued internally for eviction.
*/
public function evict($key);
}
interface QueryableStore extends Store
{
/**
* Attempts to check if a value exists in the cache for a given name.
*
* @param string $key The name of the stored value to be verified.
* @return bool Weather the value exists in the cache or not.
*/
public function contains($key);
}
interface MultiAccessStore extends Store
{
/**
* Retrievs multiple named values from cache.
*
* @param array|Traversable $keys A list of value names to retrieve.
* @return array A key/value map of stored value.
*/
public function multiRetrieve($keys);
/**
* Attempts to store a named value into the cache.
*
* @param array|Traversable $values A key/values map of values to be stored.
* @return array A key/success map.
*/
public function multiStore($values);
}
<?php
// The RI
namespace Psr\Cache\Ri;
use Psr\Cache;
abstract class EvictableStoreRi implements EvictableStore
{
/**
* {@inheritdoc}
*/
public function evict($key)
{
return $this->store($key, null, -1);
}
}
abstract class QueryableStoreRi implements QueryableStore
{
/**
* {@inheritdoc}
*/
public function contains($key)
{
return return (null !== $this->retrieve($key));
}
}
abstract class MultiAccessStoreRi implements MultiAccessStore
{
/**
* {@inheritdoc}
*/
public function multiRetrieve($keys)
{
$results = array();
if (is_array($keys) || ($keys instanceof \Traversable)) {
foreach ($keys as $key) {
$results[$key] = $this->retrieve($key);
}
}
return $results;
}
/**
* {@inheritdoc}
*/
public function multiStore($values)
{
$results = array();
if (is_array($values) || ($values instanceof \Traversable)) {
foreach ($values as $key => $value) {
$params = is_array($value) ? array_merge(array($key), $value) : array($key, $value);
$results[$key] = call_user_func_array(array($this, 'store'), $params);
}
}
return $results;
}
}
abstract class CompleteStoreRi implements Store, EvictableStore, QueryableStore, MultiAccessStore
{
/**
* {@inheritdoc}
*/
public function evict($key)
{
return $this->store($key, null, -1);
}
/**
* {@inheritdoc}
*/
public function contains($key)
{
return return (null !== $this->retrieve($key));
}
/**
* {@inheritdoc}
*/
public function multiRetrieve($keys)
{
$results = array();
if (is_array($keys) || ($keys instanceof \Traversable)) {
foreach ($keys as $key) {
$results[$key] = $this->retrieve($key);
}
}
return $results;
}
/**
* {@inheritdoc}
*/
public function multiStore($values)
{
$results = array();
if (is_array($values) || ($values instanceof \Traversable)) {
foreach ($values as $key => $value) {
$params = is_array($value) ? array_merge(array($key), $value) : array($key, $value);
$results[$key] = call_user_func_array(array($this, 'store'), $params);
}
}
return $results;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment