Skip to content

Commit

Permalink
Add PSR-compatible cached annotations reader (#404)
Browse files Browse the repository at this point in the history
  • Loading branch information
alcaeus authored Feb 28, 2021
1 parent 0cb0cd2 commit c66f06b
Show file tree
Hide file tree
Showing 6 changed files with 540 additions and 29 deletions.
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
"require": {
"php": "^7.1 || ^8.0",
"ext-tokenizer": "*",
"doctrine/lexer": "1.*"
"doctrine/lexer": "1.*",
"psr/cache": "^1 || ^2 || ^3"
},
"require-dev": {
"doctrine/cache": "1.*",
"doctrine/coding-standard": "^6.0 || ^8.1",
"phpstan/phpstan": "^0.12.20",
"phpunit/phpunit": "^7.5 || ^9.1.5"
"phpunit/phpunit": "^7.5 || ^9.1.5",
"symfony/cache": "^4.4 || ^5.2"
},
"config": {
"sort-packages": true
Expand Down
33 changes: 7 additions & 26 deletions docs/en/annotations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,39 +116,20 @@ This creates a simple annotation reader with no caching other than in
memory (in php arrays). Since parsing docblocks can be expensive you
should cache this process by using a caching reader.

You can use a file caching reader, but please note it is deprecated to
do so:
To cache annotations, you can create a ``Doctrine\Common\Annotations\PsrCachedReader``.
This reader decorates the original reader and stores all annotations in a PSR-6
cache:

.. code-block:: php
use Doctrine\Common\Annotations\FileCacheReader;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\PsrCachedReader;
$reader = new FileCacheReader(
new AnnotationReader(),
"/path/to/cache",
$debug = true
);
If you set the ``debug`` flag to ``true`` the cache reader will check
for changes in the original files, which is very important during
development. If you don't set it to ``true`` you have to delete the
directory to clear the cache. This gives faster performance, however
should only be used in production, because of its inconvenience during
development.

You can also use one of the ``Doctrine\Common\Cache\Cache`` cache
implementations to cache the annotations:

.. code-block:: php
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\CachedReader;
use Doctrine\Common\Cache\ApcCache;
$cache = ... // instantiate a PSR-6 Cache pool
$reader = new CachedReader(
$reader = new PsrCachedReader(
new AnnotationReader(),
new ApcCache(),
$cache,
$debug = true
);
Expand Down
4 changes: 4 additions & 0 deletions lib/Doctrine/Common/Annotations/CachedReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

/**
* A cache aware annotation reader.
*
* @deprecated the CachedReader is deprecated and will be removed
* in version 2.0.0 of doctrine/annotations. Please use the
* {@see \Doctrine\Common\Annotations\PsrCachedReader} instead.
*/
final class CachedReader implements Reader
{
Expand Down
2 changes: 1 addition & 1 deletion lib/Doctrine/Common/Annotations/FileCacheReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
*
* @deprecated the FileCacheReader is deprecated and will be removed
* in version 2.0.0 of doctrine/annotations. Please use the
* {@see \Doctrine\Common\Annotations\CachedReader} instead.
* {@see \Doctrine\Common\Annotations\PsrCachedReader} instead.
*/
class FileCacheReader implements Reader
{
Expand Down
232 changes: 232 additions & 0 deletions lib/Doctrine/Common/Annotations/PsrCachedReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
<?php

namespace Doctrine\Common\Annotations;

use Psr\Cache\CacheItemPoolInterface;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use Reflector;

use function array_map;
use function array_merge;
use function assert;
use function filemtime;
use function max;
use function rawurlencode;
use function time;

/**
* A cache aware annotation reader.
*/
final class PsrCachedReader implements Reader
{
/** @var Reader */
private $delegate;

/** @var CacheItemPoolInterface */
private $cache;

/** @var bool */
private $debug;

/** @var array<string, array<object>> */
private $loadedAnnotations = [];

/** @var int[] */
private $loadedFilemtimes = [];

public function __construct(Reader $reader, CacheItemPoolInterface $cache, bool $debug = false)
{
$this->delegate = $reader;
$this->cache = $cache;
$this->debug = (bool) $debug;
}

/**
* {@inheritDoc}
*/
public function getClassAnnotations(ReflectionClass $class)
{
$cacheKey = $class->getName();

if (isset($this->loadedAnnotations[$cacheKey])) {
return $this->loadedAnnotations[$cacheKey];
}

$annots = $this->fetchFromCache($cacheKey, $class, 'getClassAnnotations', $class);

return $this->loadedAnnotations[$cacheKey] = $annots;
}

/**
* {@inheritDoc}
*/
public function getClassAnnotation(ReflectionClass $class, $annotationName)
{
foreach ($this->getClassAnnotations($class) as $annot) {
if ($annot instanceof $annotationName) {
return $annot;
}
}

return null;
}

/**
* {@inheritDoc}
*/
public function getPropertyAnnotations(ReflectionProperty $property)
{
$class = $property->getDeclaringClass();
$cacheKey = $class->getName() . '$' . $property->getName();

if (isset($this->loadedAnnotations[$cacheKey])) {
return $this->loadedAnnotations[$cacheKey];
}

$annots = $this->fetchFromCache($cacheKey, $class, 'getPropertyAnnotations', $property);

return $this->loadedAnnotations[$cacheKey] = $annots;
}

/**
* {@inheritDoc}
*/
public function getPropertyAnnotation(ReflectionProperty $property, $annotationName)
{
foreach ($this->getPropertyAnnotations($property) as $annot) {
if ($annot instanceof $annotationName) {
return $annot;
}
}

return null;
}

/**
* {@inheritDoc}
*/
public function getMethodAnnotations(ReflectionMethod $method)
{
$class = $method->getDeclaringClass();
$cacheKey = $class->getName() . '#' . $method->getName();

if (isset($this->loadedAnnotations[$cacheKey])) {
return $this->loadedAnnotations[$cacheKey];
}

$annots = $this->fetchFromCache($cacheKey, $class, 'getMethodAnnotations', $method);

return $this->loadedAnnotations[$cacheKey] = $annots;
}

/**
* {@inheritDoc}
*/
public function getMethodAnnotation(ReflectionMethod $method, $annotationName)
{
foreach ($this->getMethodAnnotations($method) as $annot) {
if ($annot instanceof $annotationName) {
return $annot;
}
}

return null;
}

public function clearLoadedAnnotations(): void
{
$this->loadedAnnotations = [];
$this->loadedFilemtimes = [];
}

/** @return mixed[] */
private function fetchFromCache(
string $cacheKey,
ReflectionClass $class,
string $method,
Reflector $reflector
): array {
$cacheKey = rawurlencode($cacheKey);

$item = $this->cache->getItem($cacheKey);
if (! $item->isHit() || ($this->debug && ! $this->refresh($cacheKey, $class))) {
$this->cache->save($item->set($this->delegate->{$method}($reflector)));
}

return $item->get();
}

/**
* Used in debug mode to check if the cache is fresh.
*
* @return bool Returns true if the cache was fresh, or false if the class
* being read was modified since writing to the cache.
*/
private function refresh(string $cacheKey, ReflectionClass $class): bool
{
$lastModification = $this->getLastModification($class);
if ($lastModification === 0) {
return true;
}

$item = $this->cache->getItem('[C]' . $cacheKey);
if ($item->isHit() && $item->get() >= $lastModification) {
return true;
}

$this->cache->save($item->set(time()));

return false;
}

/**
* Returns the time the class was last modified, testing traits and parents
*/
private function getLastModification(ReflectionClass $class): int
{
$filename = $class->getFileName();

if (isset($this->loadedFilemtimes[$filename])) {
return $this->loadedFilemtimes[$filename];
}

$parent = $class->getParentClass();

$lastModification = max(array_merge(
[$filename ? filemtime($filename) : 0],
array_map(function (ReflectionClass $reflectionTrait): int {
return $this->getTraitLastModificationTime($reflectionTrait);
}, $class->getTraits()),
array_map(function (ReflectionClass $class): int {
return $this->getLastModification($class);
}, $class->getInterfaces()),
$parent ? [$this->getLastModification($parent)] : []
));

assert($lastModification !== false);

return $this->loadedFilemtimes[$filename] = $lastModification;
}

private function getTraitLastModificationTime(ReflectionClass $reflectionTrait): int
{
$fileName = $reflectionTrait->getFileName();

if (isset($this->loadedFilemtimes[$fileName])) {
return $this->loadedFilemtimes[$fileName];
}

$lastModificationTime = max(array_merge(
[$fileName ? filemtime($fileName) : 0],
array_map(function (ReflectionClass $reflectionTrait): int {
return $this->getTraitLastModificationTime($reflectionTrait);
}, $reflectionTrait->getTraits())
));

assert($lastModificationTime !== false);

return $this->loadedFilemtimes[$fileName] = $lastModificationTime;
}
}
Loading

0 comments on commit c66f06b

Please sign in to comment.