Completed
Pull Request — master (#5856)
by Marco
07:16
created

DefaultQueryCache::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 10
c 0
b 0
f 0
ccs 8
cts 8
cp 1
rs 9.4285
cc 1
eloc 7
nc 1
nop 2
crap 1
1
<?php
2
3
/*
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the MIT license. For more information, see
18
 * <http://www.doctrine-project.org>.
19
 */
20
21
namespace Doctrine\ORM\Cache;
22
23
use Doctrine\Common\Collections\ArrayCollection;
24
use Doctrine\ORM\Cache\Persister\CachedPersister;
25
use Doctrine\ORM\EntityManagerInterface;
26
use Doctrine\ORM\Query\ResultSetMapping;
27
use Doctrine\ORM\Mapping\ClassMetadata;
28
use Doctrine\ORM\PersistentCollection;
29
use Doctrine\Common\Proxy\Proxy;
30
use Doctrine\ORM\Cache;
31
use Doctrine\ORM\Query;
32
33
/**
34
 * Default query cache implementation.
35
 *
36
 * @since   2.5
37
 * @author  Fabio B. Silva <[email protected]>
38
 */
39
class DefaultQueryCache implements QueryCache
40
{
41
     /**
42
     * @var \Doctrine\ORM\EntityManagerInterface
43
     */
44
    private $em;
45
46
    /**
47
     * @var \Doctrine\ORM\UnitOfWork
48
     */
49
    private $uow;
50
51
    /**
52
     * @var \Doctrine\ORM\Cache\Region
53
     */
54
    private $region;
55
56
    /**
57
     * @var \Doctrine\ORM\Cache\QueryCacheValidator
58
     */
59
    private $validator;
60
61
    /**
62
     * @var \Doctrine\ORM\Cache\Logging\CacheLogger
63
     */
64
    protected $cacheLogger;
65
66
    /**
67
     * @var array
68
     */
69
    private static $hints = array(Query::HINT_CACHE_ENABLED => true);
70
71
    /**
72
     * @param \Doctrine\ORM\EntityManagerInterface $em     The entity manager.
73
     * @param \Doctrine\ORM\Cache\Region           $region The query region.
74
     */
75 79
    public function __construct(EntityManagerInterface $em, Region $region)
76
    {
77 79
        $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
78
79 79
        $this->em           = $em;
80 79
        $this->region       = $region;
81 79
        $this->uow          = $em->getUnitOfWork();
82 79
        $this->cacheLogger  = $cacheConfig->getCacheLogger();
83 79
        $this->validator    = $cacheConfig->getQueryValidator();
84 79
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 46
    public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array())
90
    {
91 46
        if ( ! ($key->cacheMode & Cache::MODE_GET)) {
92 3
            return null;
93
        }
94
95 45
        $entry = $this->region->get($key);
96
97 45
        if ( ! $entry instanceof QueryCacheEntry) {
98 42
            return null;
99
        }
100
101 32
        if ( ! $this->validator->isValid($key, $entry)) {
102 2
            $this->region->evict($key);
103
104 2
            return null;
105
        }
106
107 30
        $result      = array();
108 30
        $entityName  = reset($rsm->aliasMap);
109 30
        $hasRelation = ( ! empty($rsm->relationMap));
110 30
        $persister   = $this->uow->getEntityPersister($entityName);
111 30
        $region      = $persister->getCacheRegion();
112 30
        $regionName  = $region->getName();
113
114 30
        $cm = $this->em->getClassMetadata($entityName);
115
        // @TODO - move to cache hydration component
116 30
        foreach ($entry->result as $index => $entry) {
117
118 30
            if (($entityEntry = $region->get($entityKey = new EntityCacheKey($cm->rootEntityName, $entry['identifier']))) === null) {
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
119
120 2
                if ($this->cacheLogger !== null) {
121 1
                    $this->cacheLogger->entityCacheMiss($regionName, $entityKey);
122
                }
123
124 2
                return null;
125
            }
126
127 28
            if ($this->cacheLogger !== null) {
128 27
                $this->cacheLogger->entityCacheHit($regionName, $entityKey);
129
            }
130
131 28
            if ( ! $hasRelation) {
132
133 21
                $result[$index]  = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints);
134
135 21
                continue;
136
            }
137
138 7
            $data = $entityEntry->data;
139
140 7
            foreach ($entry['associations'] as $name => $assoc) {
141
142 7
                $assocPersister  = $this->uow->getEntityPersister($assoc['targetEntity']);
143 7
                $assocRegion     = $assocPersister->getCacheRegion();
144
145 7
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
146
147 4
                    if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) {
148
149 1
                        if ($this->cacheLogger !== null) {
150 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
151
                        }
152
153 1
                        $this->uow->hydrationComplete();
154
155 1
                        return null;
156
                    }
157
158 3
                    $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
159
160 3
                    if ($this->cacheLogger !== null) {
161 3
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
162
                    }
163
164 3
                    continue;
165
                }
166
167 3
                if ( ! isset($assoc['list']) || empty($assoc['list'])) {
168
                    continue;
169
                }
170
171 3
                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
172 3
                $collection  = new PersistentCollection($this->em, $targetClass, new ArrayCollection());
0 ignored issues
show
Compatibility introduced by
$targetClass of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
173
174 3
                foreach ($assoc['list'] as $assocIndex => $assocId) {
175
176 3
                    if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) {
177
178 1
                        if ($this->cacheLogger !== null) {
179 1
                            $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
180
                        }
181
182 1
                        $this->uow->hydrationComplete();
183
184 1
                        return null;
185
                    }
186
187 2
                    $element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
188
189 2
                    $collection->hydrateSet($assocIndex, $element);
190
191 2
                    if ($this->cacheLogger !== null) {
192 2
                        $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
193
                    }
194
                }
195
196 2
                $data[$name] = $collection;
197
198 2
                $collection->setInitialized(true);
199
            }
200
201 5
            $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
202
        }
203
204 26
        $this->uow->hydrationComplete();
205
206 26
        return $result;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 53
    public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = array())
213
    {
214 53
        if ($rsm->scalarMappings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rsm->scalarMappings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
215 1
            throw new CacheException("Second level cache does not support scalar results.");
216
        }
217
218 52
        if (count($rsm->entityMappings) > 1) {
219 1
            throw new CacheException("Second level cache does not support multiple root entities.");
220
        }
221
222 51
        if ( ! $rsm->isSelect) {
223 2
            throw new CacheException("Second-level cache query supports only select statements.");
224
        }
225
226 49
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
227 1
            throw new CacheException("Second level cache does not support partial entities.");
228
        }
229
230 48
        if ( ! ($key->cacheMode & Cache::MODE_PUT)) {
231 3
            return false;
232
        }
233
234 47
        $data        = array();
235 47
        $entityName  = reset($rsm->aliasMap);
236 47
        $hasRelation = ( ! empty($rsm->relationMap));
237 47
        $persister   = $this->uow->getEntityPersister($entityName);
238
239 47
        if ( ! ($persister instanceof CachedPersister)) {
240 1
            throw CacheException::nonCacheableEntity($entityName);
241
        }
242
243 46
        $region = $persister->getCacheRegion();
244
245 46
        foreach ($result as $index => $entity) {
246 46
            $identifier                     = $this->uow->getEntityIdentifier($entity);
247 46
            $data[$index]['identifier']     = $identifier;
248 46
            $data[$index]['associations']   = array();
249
250 46
            if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey = new EntityCacheKey($entityName, $identifier))) {
251
                // Cancel put result if entity put fail
252 34
                if ( ! $persister->storeEntityCache($entity, $entityKey)) {
0 ignored issues
show
Bug introduced by
The variable $entityKey does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
253 1
                    return false;
254
                }
255
            }
256
257 45
            if ( ! $hasRelation) {
258 32
                continue;
259
            }
260
261
            // @TODO - move to cache hydration components
262 13
            foreach ($rsm->relationMap as $alias => $name) {
263 13
                $metadata = $this->em->getClassMetadata($rsm->aliasMap[$rsm->parentAliasMap[$alias]]);
264 13
                $className = $metadata->getName();
265
266 13
                if (! $entity instanceof $className) {
267
                    // this alias is not the root alias, therefore we skip it. $entity is always the root of the selection here
268
                    // @TODO should actually cache all aliases
269 1
                    continue;
270
                }
271
272 13
                $assoc = $metadata->associationMappings[$name];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
273
274 13
                if (($assocValue = $metadata->getFieldValue($entity, $name)) === null || $assocValue instanceof Proxy) {
275 1
                    continue;
276
                }
277
278 12
                $assocPersister  = $this->uow->getEntityPersister($assoc['targetEntity']);
279 12
                $assocRegion     = $assocPersister->getCacheRegion();
280 12
                $assocMetadata   = $assocPersister->getClassMetadata();
281
282
                // Handle *-to-one associations
283 12
                if ($assoc['type'] & ClassMetadata::TO_ONE) {
284
285 7
                    $assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
286
287 7
                    if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) {
288
289
                        // Cancel put result if association entity put fail
290 7
                        if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
291 1
                            return false;
292
                        }
293
                    }
294
295 6
                    $data[$index]['associations'][$name] = array(
296 6
                        'targetEntity'  => $assocMetadata->rootEntityName,
297 6
                        'identifier'    => $assocIdentifier,
298 6
                        'type'          => $assoc['type']
299
                    );
300
301 6
                    continue;
302
                }
303
304
                // Handle *-to-many associations
305 5
                $list = array();
306
307 5
                foreach ($assocValue as $assocItemIndex => $assocItem) {
308 5
                    $assocIdentifier = $this->uow->getEntityIdentifier($assocItem);
309
310 5
                    if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) {
311
312
                        // Cancel put result if entity put fail
313 5
                        if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
314 1
                            return false;
315
                        }
316
                    }
317
318 4
                    $list[$assocItemIndex] = $assocIdentifier;
319
                }
320
321 4
                $data[$index]['associations'][$name] = array(
322 4
                    'targetEntity'  => $assocMetadata->rootEntityName,
323 4
                    'type'          => $assoc['type'],
324 11
                    'list'          => $list,
325
                );
326
            }
327
        }
328
329 43
        return $this->region->put($key, new QueryCacheEntry($data));
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     */
335 46
    public function clear()
336
    {
337 46
        return $this->region->evictAll();
338
    }
339
340
    /**
341
     * {@inheritdoc}
342
     */
343 24
    public function getRegion()
344
    {
345 24
        return $this->region;
346
    }
347
}
348