Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Added PSR-6 adapter #54

Merged
merged 2 commits into from
Jun 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
"php": "^5.5 || ^7.0",
"zendframework/zend-stdlib": "^2.7 || ^3.0",
"zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3",
"zendframework/zend-eventmanager": "^2.6.2 || ^3.0"
"zendframework/zend-eventmanager": "^2.6.2 || ^3.0",
"psr/cache": "^1.0"
},
"require-dev": {
"zendframework/zend-serializer": "^2.6",
"zendframework/zend-session": "^2.5",
"cache/integration-tests": "^0.8.0",
"fabpot/php-cs-fixer": "1.7.*",
"phpunit/PHPUnit": "~4.0"
},
Expand Down
132 changes: 132 additions & 0 deletions doc/book/psr6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Zend\\Cache\\Psr\\CacheItemPoolAdapter

## Overview

The `Zend\Cache\Psr\CacheItemPoolAdapter` provides a [PSR-6](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-6-cache.md)
compliant wrapper for supported storage adapters.

PSR-6 specifies a common interface to cache storage, enabling developers to switch between implementations without
having to worry about any behind-the-scenes differences between them.


## Quick Start

To use the pool, instantiate your storage as normal, then pass it to the `CacheItemPoolAdapter`.

```php
use Zend\Cache\StorageFactory;
use Zend\Cache\Psr\CacheItemPoolAdapter;

$storage = StorageFactory::factory([
'adapter' => [
'name' => 'apc',
'options' => [],
],
]);

$pool = new CacheItemPoolAdapter($storage);

// attempt to get an item from cache
$item = $pool->getItem('foo');

// check whether item was found
if (! $item->isHit()) {
// ...
// perform expensive operation to calculate $value for 'foo'
// ...

$item->set($value);
$pool->save($item);
}

// use the value of the item
echo $item->get();
```

Note that you will always get back a `CacheItem` object, whether it was found in cache or not: this is so `false`-y
values like an empty string, `null`, or `false` can be stored. Always check `isHit()` to determine if the item was
found.


## Supported adapters

The PSR-6 specification requires that the underlying storage support time-to-live (TTL), which is set when the
item is saved. For this reason the following adapters cannot be used: `Dba`, `Filesystem`, `Memory` and `Session`. The
`XCache` adapter calculates TTLs based on the request time, not the time the item is actually persisted, which means
that it also cannot be used.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is confusing because:

  • The filesystem and memory adapter supports TTL but it's based on a stored last-modification-time + runtime TTL setting
    • I'll open an issue for the Memory adapter to change this because this should be a simple change and could also improve performance but it's a BC break.
    • For the filesystem adapter this would be worst because it would be required to read the file before to know it's expired.
  • The XCache adapter is not the only adapter calculating TTLs based on request time. APC/APCu also have this behavior if apc.use_request_time which is enabled be default.

EDIT: Uuups you mentioned the APC behavior already in the next paragraph


In addition adapters must support the `Zend\Cache\FlushableInterface`. All the current `Zend\Cache\Storage\Adapter`s
fulfil this requirement.

Attempting to use an unsupported adapter will throw an exception implementing `Psr\Cache\CacheException`.

### Quirks

#### APC

You cannot set the [`apc.use_request_time`](http://php.net/manual/en/apc.configuration.php#ini.apc.use-request-time)
ini setting with the APC adapter: the specification requires that all TTL values are calculated from when the item is
actually saved to storage. If this is set when you instantiate the pool it will throw an exception implementing
`Psr\Cache\CacheException`. Changing the setting after you have instantiated the pool will result in non-standard
behaviour.


## Logging errors

The specification [states](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-6-cache.md#error-handling):

> While caching is often an important part of application performance, it should never be a critical part of application
> functionality. Thus, an error in a cache system SHOULD NOT result in application failure.

Once you've got your pool instance, almost all exceptions thrown by the storage will be caught and ignored. The only
storage exceptions that bubble up implement `Psr\Cache\InvalidArgumentException` and are typically caused by invalid
key errors. To be PSR-6 compliant, cache keys must not contain the following characters: `{}()/\@:`. However different
storage adapters may have further restrictions. Check the documentation for your particular adapter to be sure.

We strongly recommend tracking exceptions caught from storage, either by logging them or recording them in some other
way. Doing so is as simple as adding an [`ExceptionHandler` plugin](zend.cache.storage.plugin.html#3.4). Say you have a
[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) compliant logger
called `$logger`:


```php
$cacheLogger = function (\Exception $e) use ($logger) {
$message = sprintf(
'[CACHE] %s:%s %s "%s"',
$exception->getFile(),
$exception->getLine(),
$exception->getCode(),
$exception->getMessage()
);
$logger->error($message);
};
}
$storage = StorageFactory::factory([
'adapter' => [
'name' => 'apc',
],
'plugins' => [
'exceptionhandler' => [
'exception_callback' => $cacheLogger,
'throw_exceptions' => true,
],
],
]);

$pool = new CacheItemPoolAdapter($storage);
```

Note that `throw_exceptions` should always be `true` (the default) or you will not get the correct return values from
calls on the pool such as `save()`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we catch the exceptions and return false from these methods in case of errors, as per the spec. If they're not thrown we'll always return true. I'll clarify the docs.



## Supported data types

As per [the specification](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-6-cache.md#data), the
following data types can be stored in cache: `string`, `integer`, `float`, `boolean`, `null`, `array`, `object` and be
returned as a value with exactly the same type.

Not all adapters can natively store all these types. For instance, Redis stores booleans and integers as a string. Where
this is the case *all* values will be automatically run through `serialize()` on save and `unserialize()` on get: you
do not need to use a `Zend\Cache\Storage\Plugin\Serializer` plugin.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... but you can if you like to change the serializer to igbinary.


16 changes: 16 additions & 0 deletions doc/bookdown.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"title": "Zend\\Cache",
"target": "html/",
"content": [
"book/zend.cache.storage.adapter.md",
"book/zend.cache.storage.capabilities.md",
"book/zend.cache.storage.plugin.md",
"book/zend.cache.pattern.md",
"book/zend.cache.pattern.callback-cache.md",
"book/zend.cache.pattern.class-cache.md",
"book/zend.cache.pattern.object-cache.md",
"book/zend.cache.pattern.output-cache.md",
"book/zend.cache.pattern.capture-cache.md",
"book/zend.cache.psr.cacheitempooladapter.md"
]
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pages:
- ObjectCache: pattern/object-cache.md
- OutputCache: pattern/output-cache.md
- CaptureCache: pattern/capture-cache.md
- PSR-6: psr6.md
site_name: zend-cache
site_description: Zend\Cache
repo_url: 'https://github.com/zendframework/zend-cache'
Expand Down
16 changes: 16 additions & 0 deletions src/Psr/CacheException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/).
*
* @link http://github.com/zendframework/zend-cache for the canonical source repository
* @copyright Copyright (c) 2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/

namespace Zend\Cache\Psr;

use Psr\Cache\CacheException as CacheExceptionInterface;

class CacheException extends \RuntimeException implements CacheExceptionInterface
{
}
174 changes: 174 additions & 0 deletions src/Psr/CacheItem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php
/**
* Zend Framework (http://framework.zend.com/).
*
* @link http://github.com/zendframework/zend-cache for the canonical source repository
* @copyright Copyright (c) 2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/

namespace Zend\Cache\Psr;

use DateInterval;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Psr\Cache\CacheItemInterface;

final class CacheItem implements CacheItemInterface
{
/**
* Cache key
* @var string
*/
private $key;

/**
* Cache value
* @var mixed|null
*/
private $value;

/**
* True if the cache item lookup resulted in a cache hit or if they item is deferred or successfully saved
* @var bool
*/
private $isHit = false;

/**
* Timestamp item will expire at if expiresAt() called, null otherwise
* @var int|null
*/
private $expiration = null;

/**
* Seconds after item is stored it will expire at if expiresAfter() called, null otherwise
* @var int|null
*/
private $ttl = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please can you add code comments for each of the properties - thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.


/**
* @var DateTimeZone
*/
private $tz;

/**
* Constructor.
*
* @param string $key
* @param mixed $value
* @param bool $isHit
*/
public function __construct($key, $value, $isHit)
{
$this->key = $key;
$this->value = $isHit ? $value : null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null is a valid value for PSR-6

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, which is why you need to check isHit() - afaik the value should be null if its not a hit.

$this->isHit = $isHit;
$this->tz = new DateTimeZone('UTC');
}

/**
* {@inheritdoc}
*/
public function getKey()
{
return $this->key;
}

/**
* {@inheritdoc}
*/
public function get()
{
return $this->value;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to check if isHit before you return anything

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value is set to null in the constructor if the item isn't a hit.

}

/**
* {@inheritdoc}
*/
public function isHit()
{
if (! $this->isHit) {
return false;
}
$ttl = $this->getTtl();
return $ttl === null || $ttl > 0;
}

/**
* Sets isHit value
*
* This function is called by CacheItemPoolAdapter::saveDeferred() and is not intended for use by other calling
* code.
*
* @param boolean $isHit
* @return $this
*/
public function setIsHit($isHit)
{
$this->isHit = $isHit;

return $this;
}

/**
* {@inheritdoc}
*/
public function set($value)
{
$this->value = $value;

return $this;
}

/**
* {@inheritdoc}
*/
public function expiresAt($expiration)
{
if (! ($expiration === null || $expiration instanceof DateTimeInterface)) {
throw new InvalidArgumentException('$expiration must be null or an instance of DateTimeInterface');
}

$this->expiration = $expiration instanceof DateTimeInterface ? $expiration->getTimestamp() : null;
$this->ttl = null;

return $this;
}

/**
* {@inheritdoc}
*/
public function expiresAfter($time)
{
if ($time instanceof DateInterval) {
$end = new DateTime('now', $this->tz);
$end->add($time);
$this->ttl = $end->getTimestamp() - time();
} elseif (is_int($time) || $time === null) {
$this->ttl = $time;
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm missing the handling of null to get the default TTL defined by adapter

throw new InvalidArgumentException(sprintf('Invalid $time "%s"', gettype($time)));
}

$this->expiration = null;

return $this;
}

/**
* Returns number of seconds until item expires
*
* If NULL, the pool should use the default TTL for the storage adapter. If <= 0, the item has expired.
*
* @return int|null
*/
public function getTtl()
{
$ttl = $this->ttl;
if ($this->expiration !== null) {
$ttl = $this->expiration - time();
}
return $ttl;
}
}
Loading