diff --git a/.travis.yml b/.travis.yml index 5ecefb6cc..3b16e5551 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ php: - 5.5 - 5.6 - 7.0 + - nightly - hhvm env: @@ -63,6 +64,8 @@ matrix: exclude: - php: hhvm env: DB=pgsql # driver for PostgreSQL currently unsupported by HHVM, requires 3rd party dependency + allow_failures: + - php: nightly sudo: false diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index 07c9d8f43..dabf1721e 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -233,6 +233,7 @@ class DefaultQueryCache implements QueryCache $data = array(); $entityName = reset($rsm->aliasMap); + $rootAlias = key($rsm->aliasMap); $hasRelation = ( ! empty($rsm->relationMap)); $persister = $this->uow->getEntityPersister($entityName); @@ -244,10 +245,11 @@ class DefaultQueryCache implements QueryCache foreach ($result as $index => $entity) { $identifier = $this->uow->getEntityIdentifier($entity); + $entityKey = new EntityCacheKey($entityName, $identifier); $data[$index]['identifier'] = $identifier; $data[$index]['associations'] = array(); - if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey = new EntityCacheKey($entityName, $identifier))) { + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) { // Cancel put result if entity put fail if ( ! $persister->storeEntityCache($entity, $entityKey)) { return false; @@ -260,67 +262,170 @@ class DefaultQueryCache implements QueryCache // @TODO - move to cache hydration components foreach ($rsm->relationMap as $alias => $name) { - $metadata = $this->em->getClassMetadata($rsm->aliasMap[$rsm->parentAliasMap[$alias]]); - $assoc = $metadata->associationMappings[$name]; + $parentAlias = $rsm->parentAliasMap[$alias]; + $parentClass = $rsm->aliasMap[$parentAlias]; + $metadata = $this->em->getClassMetadata($parentClass); + $assoc = $metadata->associationMappings[$name]; + $assocValue = $this->getAssociationValue($rsm, $alias, $entity); - if (($assocValue = $metadata->getFieldValue($entity, $name)) === null || $assocValue instanceof Proxy) { + if ($assocValue === null) { continue; } - $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); - $assocRegion = $assocPersister->getCacheRegion(); - $assocMetadata = $assocPersister->getClassMetadata(); - - // Handle *-to-one associations - if ($assoc['type'] & ClassMetadata::TO_ONE) { - - $assocIdentifier = $this->uow->getEntityIdentifier($assocValue); - - if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) { - - // Cancel put result if association entity put fail - if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) { - return false; - } + // root entity association + if ($rootAlias === $parentAlias) { + // Cancel put result if association put fail + if ( ($assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue)) === null) { + return false; } - $data[$index]['associations'][$name] = array( - 'targetEntity' => $assocMetadata->rootEntityName, - 'identifier' => $assocIdentifier, - 'type' => $assoc['type'] - ); + $data[$index]['associations'][$name] = $assocInfo; continue; } - // Handle *-to-many associations - $list = array(); - - foreach ($assocValue as $assocItemIndex => $assocItem) { - $assocIdentifier = $this->uow->getEntityIdentifier($assocItem); - - if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) { - - // Cancel put result if entity put fail - if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) { - return false; - } + // store single nested association + if ( ! is_array($assocValue)) { + // Cancel put result if association put fail + if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) { + return false; } - $list[$assocItemIndex] = $assocIdentifier; + continue; } - $data[$index]['associations'][$name] = array( - 'targetEntity' => $assocMetadata->rootEntityName, - 'type' => $assoc['type'], - 'list' => $list, - ); + // store array of nested association + foreach ($assocValue as $aVal) { + // Cancel put result if association put fail + if ($this->storeAssociationCache($key, $assoc, $aVal) === null) { + return false; + } + } } } return $this->region->put($key, new QueryCacheEntry($data)); } + /** + * @param \Doctrine\ORM\Cache\QueryCacheKey $key + * @param array $assoc + * @param mixed $assocValue + * + * @return array|null + */ + private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocValue) + { + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + $assocMetadata = $assocPersister->getClassMetadata(); + $assocRegion = $assocPersister->getCacheRegion(); + + // Handle *-to-one associations + if ($assoc['type'] & ClassMetadata::TO_ONE) { + $assocIdentifier = $this->uow->getEntityIdentifier($assocValue); + $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); + + if ( ! $assocValue instanceof Proxy && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) { + // Entity put fail + if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) { + return null; + } + } + + return array( + 'targetEntity' => $assocMetadata->rootEntityName, + 'identifier' => $assocIdentifier, + 'type' => $assoc['type'] + ); + } + + // Handle *-to-many associations + $list = array(); + + foreach ($assocValue as $assocItemIndex => $assocItem) { + $assocIdentifier = $this->uow->getEntityIdentifier($assocItem); + $entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) { + // Entity put fail + if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) { + return null; + } + } + + $list[$assocItemIndex] = $assocIdentifier; + } + + return array( + 'targetEntity' => $assocMetadata->rootEntityName, + 'type' => $assoc['type'], + 'list' => $list, + ); + } + + /** + * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * @param string $assocAlias + * @param object $entity + * + * @return array|object + */ + private function getAssociationValue(ResultSetMapping $rsm, $assocAlias, $entity) + { + $path = array(); + $alias = $assocAlias; + + while (isset($rsm->parentAliasMap[$alias])) { + $parent = $rsm->parentAliasMap[$alias]; + $field = $rsm->relationMap[$alias]; + $class = $rsm->aliasMap[$parent]; + + array_unshift($path, array( + 'field' => $field, + 'class' => $class + )); + + $alias = $parent; + } + + return $this->getAssociationPathValue($entity, $path); + } + + /** + * @param mixed $value + * @param array $path + * + * @return array|object|null + */ + private function getAssociationPathValue($value, array $path) + { + $mapping = array_shift($path); + $metadata = $this->em->getClassMetadata($mapping['class']); + $assoc = $metadata->associationMappings[$mapping['field']]; + $value = $metadata->getFieldValue($value, $mapping['field']); + + if ($value === null) { + return null; + } + + if (empty($path)) { + return $value; + } + + // Handle *-to-one associations + if ($assoc['type'] & ClassMetadata::TO_ONE) { + return $this->getAssociationPathValue($value, $path); + } + + $values = array(); + + foreach ($value as $item) { + $values[] = $this->getAssociationPathValue($item, $path); + } + + return $values; + } + /** * {@inheritdoc} */ diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index 2de9c6698..b183eaed4 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -12,6 +12,7 @@ use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\Tests\Models\Cache\Country; use Doctrine\Tests\Models\Cache\City; use Doctrine\Tests\Models\Cache\State; +use Doctrine\Tests\Models\Cache\Restaurant; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Tests\Models\Generic\BooleanModel; use Doctrine\ORM\Cache\EntityCacheEntry; @@ -67,7 +68,7 @@ class DefaultQueryCacheTest extends OrmTestCase { $this->assertSame($this->region, $this->queryCache->getRegion()); } - + public function testClearShouldEvictRegion() { $this->queryCache->clear(); @@ -176,13 +177,13 @@ class DefaultQueryCacheTest extends OrmTestCase $countryClass->setFieldValue($country, 'id', $i*3); $uow->registerManaged($country, array('id' => $country->getId()), array('name' => $country->getName())); - $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $city->getName(), 'country' => $country)); + $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $state->getName(), 'country' => $country)); $uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => $state)); } $this->assertTrue($this->queryCache->put($key, $rsm, $result)); $this->assertArrayHasKey('put', $this->region->calls); - $this->assertCount(9, $this->region->calls['put']); + $this->assertCount(13, $this->region->calls['put']); $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']); $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']); @@ -192,7 +193,11 @@ class DefaultQueryCacheTest extends OrmTestCase $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][5]['key']); $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][6]['key']); $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][7]['key']); - $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][8]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][8]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][9]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][10]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][11]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][12]['key']); } public function testPutToOneAssociationNullQueryResult() @@ -482,6 +487,50 @@ class DefaultQueryCacheTest extends OrmTestCase $this->assertNull($this->queryCache->get($key, $rsm)); } + public function testGetAssociationValue() + { + $reflection = new \ReflectionMethod($this->queryCache, 'getAssociationValue'); + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 0); + + $reflection->setAccessible(true); + + $germany = new Country("Germany"); + $bavaria = new State("Bavaria", $germany); + $wurzburg = new City("Würzburg", $bavaria); + $munich = new City("Munich", $bavaria); + + $bavaria->addCity($munich); + $bavaria->addCity($wurzburg); + + $munich->addAttraction(new Restaurant('Reinstoff', $munich)); + $munich->addAttraction(new Restaurant('Schneider Weisse', $munich)); + $wurzburg->addAttraction(new Restaurant('Fischers Fritz', $wurzburg)); + + $rsm->addRootEntityFromClassMetadata(State::CLASSNAME, 's'); + $rsm->addJoinedEntityFromClassMetadata(City::CLASSNAME, 'c', 's', 'cities', array( + 'id' => 'c_id', + 'name' => 'c_name' + )); + $rsm->addJoinedEntityFromClassMetadata(Restaurant::CLASSNAME, 'a', 'c', 'attractions', array( + 'id' => 'a_id', + 'name' => 'a_name' + )); + + $cities = $reflection->invoke($this->queryCache, $rsm, 'c', $bavaria); + $attractions = $reflection->invoke($this->queryCache, $rsm, 'a', $bavaria); + + $this->assertCount(2, $cities); + $this->assertCount(2, $attractions); + + $this->assertInstanceOf('Doctrine\Common\Collections\Collection', $cities); + $this->assertInstanceOf('Doctrine\Common\Collections\Collection', $attractions[0]); + $this->assertInstanceOf('Doctrine\Common\Collections\Collection', $attractions[1]); + + $this->assertCount(2, $attractions[0]); + $this->assertCount(1, $attractions[1]); + } + /** * @expectedException Doctrine\ORM\Cache\CacheException * @expectedExceptionMessage Second level cache does not support scalar results. diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersisterTest.php index 6cf3fea3b..df1f6ac59 100644 --- a/tests/Doctrine/Tests/ORM/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersisterTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/Entity/NonStrictReadWriteCachedEntityPersisterTest.php @@ -2,14 +2,14 @@ namespace Doctrine\Tests\ORM\Cache\Persister\Entity; +use Doctrine\ORM\Cache\EntityCacheEntry; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\Persister\Entity\NonStrictReadWriteCachedEntityPersister; use Doctrine\ORM\Cache\Region; use Doctrine\ORM\EntityManager; -use Doctrine\Tests\Models\Cache\Country; -use Doctrine\ORM\Cache\EntityCacheKey; -use Doctrine\ORM\Cache\EntityCacheEntry; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Persisters\Entity\EntityPersister; -use Doctrine\ORM\Cache\Persister\Entity\NonStrictReadWriteCachedEntityPersister; +use Doctrine\Tests\Models\Cache\Country; /** * @group DDC-2183 @@ -28,7 +28,7 @@ class NonStrictReadWriteCachedEntityPersisterTest extends AbstractEntityPersiste { $entity = new Country("Foo"); $persister = $this->createPersisterDefault(); - $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\Entity\ReadWriteCachedEntityPersister', 'queuedCache'); + $property = new \ReflectionProperty($persister, 'queuedCache'); $property->setAccessible(true); @@ -50,7 +50,7 @@ class NonStrictReadWriteCachedEntityPersisterTest extends AbstractEntityPersiste $persister = $this->createPersisterDefault(); $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); $entry = new EntityCacheEntry(Country::CLASSNAME, array('id'=>1, 'name'=>'Foo')); - $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\Entity\ReadWriteCachedEntityPersister', 'queuedCache'); + $property = new \ReflectionProperty($persister, 'queuedCache'); $property->setAccessible(true); @@ -87,7 +87,7 @@ class NonStrictReadWriteCachedEntityPersisterTest extends AbstractEntityPersiste $persister = $this->createPersisterDefault(); $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); $entry = new EntityCacheEntry(Country::CLASSNAME, array('id'=>1, 'name'=>'Foo')); - $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\Entity\ReadWriteCachedEntityPersister', 'queuedCache'); + $property = new \ReflectionProperty($persister, 'queuedCache'); $property->setAccessible(true); @@ -115,7 +115,7 @@ class NonStrictReadWriteCachedEntityPersisterTest extends AbstractEntityPersiste $entity = new Country("Foo"); $persister = $this->createPersisterDefault(); $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); - $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\Entity\ReadWriteCachedEntityPersister', 'queuedCache'); + $property = new \ReflectionProperty($persister, 'queuedCache'); $property->setAccessible(true); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index f5e1df0a8..97d8ce82a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -3,10 +3,11 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\ORM\AbstractQuery; -use Doctrine\Tests\Models\Cache\Country; use Doctrine\ORM\Query\ResultSetMapping; -use Doctrine\Tests\Models\Cache\State; use Doctrine\Tests\Models\Cache\City; +use Doctrine\Tests\Models\Cache\State; +use Doctrine\Tests\Models\Cache\Country; +use Doctrine\Tests\Models\Cache\Attraction; use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\ORM\Cache\EntityCacheKey; use Doctrine\ORM\Cache\EntityCacheEntry; @@ -189,7 +190,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $countryId2 = $this->countries[1]->getId(); $countryName1 = $this->countries[0]->getName(); $countryName2 = $this->countries[1]->getName(); - + $key1 = new EntityCacheKey(Country::CLASSNAME, array('id'=>$countryId1)); $key2 = new EntityCacheKey(Country::CLASSNAME, array('id'=>$countryId2)); $entry1 = new EntityCacheEntry(Country::CLASSNAME, array('id'=>$countryId1, 'name'=>'outdated')); @@ -283,6 +284,89 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); } + /** + * @group 5854 + */ + public function testMultipleNestedDQLAliases() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $queryRegionName = $this->getDefaultQueryRegionName(); + $cityRegionName = $this->getEntityRegion(City::CLASSNAME); + $stateRegionName = $this->getEntityRegion(State::CLASSNAME); + $attractionRegionName = $this->getEntityRegion(Attraction::CLASSNAME); + + $this->secondLevelCacheLogger->clearStats(); + $this->evictRegions(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT s, c, a FROM Doctrine\Tests\Models\Cache\State s JOIN s.cities c JOIN c.attractions a'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(2, $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[2]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[3]->getId())); + + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $this->attractions[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $this->attractions[1]->getId())); + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $this->attractions[2]->getId())); + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $this->attractions[3]->getId())); + + $this->assertInstanceOf(State::CLASSNAME, $result1[0]); + $this->assertInstanceOf(State::CLASSNAME, $result1[1]); + + $this->assertCount(2, $result1[0]->getCities()); + $this->assertCount(2, $result1[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(1)); + + $this->assertCount(2, $result1[0]->getCities()->get(0)->getAttractions()); + $this->assertCount(2, $result1[0]->getCities()->get(1)->getAttractions()); + $this->assertCount(2, $result1[1]->getCities()->get(0)->getAttractions()); + $this->assertCount(1, $result1[1]->getCities()->get(1)->getAttractions()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(2, $result2); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $result2[0]); + $this->assertInstanceOf(State::CLASSNAME, $result2[1]); + + $this->assertCount(2, $result2[0]->getCities()); + $this->assertCount(2, $result2[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(1)); + + $this->assertCount(2, $result2[0]->getCities()->get(0)->getAttractions()); + $this->assertCount(2, $result2[0]->getCities()->get(1)->getAttractions()); + $this->assertCount(2, $result2[1]->getCities()->get(0)->getAttractions()); + $this->assertCount(1, $result2[1]->getCities()->get(1)->getAttractions()); + } + public function testBasicQueryParams() { $this->evictRegions(); @@ -340,7 +424,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($this->countries[1]->getId(), $result1[1]->getId()); $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); $this->assertEquals($this->countries[1]->getName(), $result1[1]->getName()); - + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName()));