1<?php
2/*
3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 *
15 * This software consists of voluntary contributions made by many individuals
16 * and is licensed under the MIT license. For more information, see
17 * <http://www.doctrine-project.org>.
18 */
19
20namespace Doctrine\Common\Annotations;
21
22use Doctrine\Common\Cache\Cache;
23use ReflectionClass;
24
25/**
26 * A cache aware annotation reader.
27 *
28 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
29 * @author Benjamin Eberlei <kontakt@beberlei.de>
30 */
31final class CachedReader implements Reader
32{
33    /**
34     * @var Reader
35     */
36    private $delegate;
37
38    /**
39     * @var Cache
40     */
41    private $cache;
42
43    /**
44     * @var boolean
45     */
46    private $debug;
47
48    /**
49     * @var array
50     */
51    private $loadedAnnotations = array();
52
53    /**
54     * Constructor.
55     *
56     * @param Reader $reader
57     * @param Cache  $cache
58     * @param bool   $debug
59     */
60    public function __construct(Reader $reader, Cache $cache, $debug = false)
61    {
62        $this->delegate = $reader;
63        $this->cache = $cache;
64        $this->debug = (boolean) $debug;
65    }
66
67    /**
68     * {@inheritDoc}
69     */
70    public function getClassAnnotations(ReflectionClass $class)
71    {
72        $cacheKey = $class->getName();
73
74        if (isset($this->loadedAnnotations[$cacheKey])) {
75            return $this->loadedAnnotations[$cacheKey];
76        }
77
78        if (false === ($annots = $this->fetchFromCache($cacheKey, $class))) {
79            $annots = $this->delegate->getClassAnnotations($class);
80            $this->saveToCache($cacheKey, $annots);
81        }
82
83        return $this->loadedAnnotations[$cacheKey] = $annots;
84    }
85
86    /**
87     * {@inheritDoc}
88     */
89    public function getClassAnnotation(ReflectionClass $class, $annotationName)
90    {
91        foreach ($this->getClassAnnotations($class) as $annot) {
92            if ($annot instanceof $annotationName) {
93                return $annot;
94            }
95        }
96
97        return null;
98    }
99
100    /**
101     * {@inheritDoc}
102     */
103    public function getPropertyAnnotations(\ReflectionProperty $property)
104    {
105        $class = $property->getDeclaringClass();
106        $cacheKey = $class->getName().'$'.$property->getName();
107
108        if (isset($this->loadedAnnotations[$cacheKey])) {
109            return $this->loadedAnnotations[$cacheKey];
110        }
111
112        if (false === ($annots = $this->fetchFromCache($cacheKey, $class))) {
113            $annots = $this->delegate->getPropertyAnnotations($property);
114            $this->saveToCache($cacheKey, $annots);
115        }
116
117        return $this->loadedAnnotations[$cacheKey] = $annots;
118    }
119
120    /**
121     * {@inheritDoc}
122     */
123    public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName)
124    {
125        foreach ($this->getPropertyAnnotations($property) as $annot) {
126            if ($annot instanceof $annotationName) {
127                return $annot;
128            }
129        }
130
131        return null;
132    }
133
134    /**
135     * {@inheritDoc}
136     */
137    public function getMethodAnnotations(\ReflectionMethod $method)
138    {
139        $class = $method->getDeclaringClass();
140        $cacheKey = $class->getName().'#'.$method->getName();
141
142        if (isset($this->loadedAnnotations[$cacheKey])) {
143            return $this->loadedAnnotations[$cacheKey];
144        }
145
146        if (false === ($annots = $this->fetchFromCache($cacheKey, $class))) {
147            $annots = $this->delegate->getMethodAnnotations($method);
148            $this->saveToCache($cacheKey, $annots);
149        }
150
151        return $this->loadedAnnotations[$cacheKey] = $annots;
152    }
153
154    /**
155     * {@inheritDoc}
156     */
157    public function getMethodAnnotation(\ReflectionMethod $method, $annotationName)
158    {
159        foreach ($this->getMethodAnnotations($method) as $annot) {
160            if ($annot instanceof $annotationName) {
161                return $annot;
162            }
163        }
164
165        return null;
166    }
167
168    /**
169     * Clears loaded annotations.
170     *
171     * @return void
172     */
173    public function clearLoadedAnnotations()
174    {
175        $this->loadedAnnotations = array();
176    }
177
178    /**
179     * Fetches a value from the cache.
180     *
181     * @param string          $cacheKey The cache key.
182     * @param ReflectionClass $class    The related class.
183     *
184     * @return mixed The cached value or false when the value is not in cache.
185     */
186    private function fetchFromCache($cacheKey, ReflectionClass $class)
187    {
188        if (($data = $this->cache->fetch($cacheKey)) !== false) {
189            if (!$this->debug || $this->isCacheFresh($cacheKey, $class)) {
190                return $data;
191            }
192        }
193
194        return false;
195    }
196
197    /**
198     * Saves a value to the cache.
199     *
200     * @param string $cacheKey The cache key.
201     * @param mixed  $value    The value.
202     *
203     * @return void
204     */
205    private function saveToCache($cacheKey, $value)
206    {
207        $this->cache->save($cacheKey, $value);
208        if ($this->debug) {
209            $this->cache->save('[C]'.$cacheKey, time());
210        }
211    }
212
213    /**
214     * Checks if the cache is fresh.
215     *
216     * @param string           $cacheKey
217     * @param ReflectionClass $class
218     *
219     * @return boolean
220     */
221    private function isCacheFresh($cacheKey, ReflectionClass $class)
222    {
223        if (null === $lastModification = $this->getLastModification($class)) {
224            return true;
225        }
226
227        return $this->cache->fetch('[C]'.$cacheKey) >= $lastModification;
228    }
229
230    /**
231     * Returns the time the class was last modified, testing traits and parents
232     *
233     * @param ReflectionClass $class
234     * @return int
235     */
236    private function getLastModification(ReflectionClass $class)
237    {
238        $filename = $class->getFileName();
239        $parent   = $class->getParentClass();
240
241        return max(array_merge(
242            [$filename ? filemtime($filename) : 0],
243            array_map([$this, 'getTraitLastModificationTime'], $class->getTraits()),
244            array_map([$this, 'getLastModification'], $class->getInterfaces()),
245            $parent ? [$this->getLastModification($parent)] : []
246        ));
247    }
248
249    /**
250     * @param ReflectionClass $reflectionTrait
251     * @return int
252     */
253    private function getTraitLastModificationTime(ReflectionClass $reflectionTrait)
254    {
255        $fileName = $reflectionTrait->getFileName();
256
257        return max(array_merge(
258            [$fileName ? filemtime($fileName) : 0],
259            array_map([$this, 'getTraitLastModificationTime'], $reflectionTrait->getTraits())
260        ));
261    }
262}
263