Skip to content

Commit

Permalink
Support for PSR-6 result caches
Browse files Browse the repository at this point in the history
Co-authored-by: Grégoire Paris <[email protected]>
  • Loading branch information
derrabus and greg0ire committed Sep 11, 2021
1 parent dc1336d commit 996fa77
Show file tree
Hide file tree
Showing 15 changed files with 450 additions and 152 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
include:
- php-version: "8.0"
dbal-version: "2.13"
- php-version: "8.0"
dbal-version: "3.2@dev"

steps:
- name: "Checkout"
Expand Down
137 changes: 112 additions & 25 deletions lib/Doctrine/ORM/AbstractQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Doctrine\ORM;

use Countable;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Cache\QueryCacheProfile;
Expand All @@ -20,10 +22,12 @@
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\Mapping\MappingException;
use Psr\Cache\CacheItemPoolInterface;
use Traversable;

use function array_map;
use function array_shift;
use function assert;
use function count;
use function is_array;
use function is_numeric;
Expand All @@ -32,6 +36,7 @@
use function iterator_count;
use function iterator_to_array;
use function ksort;
use function method_exists;
use function reset;
use function serialize;
use function sha1;
Expand Down Expand Up @@ -121,7 +126,7 @@ abstract class AbstractQuery
*/
protected $_expireResultCache = false;

/** @var QueryCacheProfile */
/** @var QueryCacheProfile|null */
protected $_hydrationCacheProfile;

/**
Expand Down Expand Up @@ -529,9 +534,25 @@ private function translateNamespaces(Query\ResultSetMapping $rsm): void
*/
public function setHydrationCacheProfile(?QueryCacheProfile $profile = null)
{
if ($profile !== null && ! $profile->getResultCacheDriver()) {
$resultCacheDriver = $this->_em->getConfiguration()->getHydrationCacheImpl();
$profile = $profile->setResultCacheDriver($resultCacheDriver);
if ($profile === null) {
$this->_hydrationCacheProfile = null;

return $this;
}

// DBAL < 3.2
if (! method_exists(QueryCacheProfile::class, 'setResultCache')) {
if (! $profile->getResultCacheDriver()) {
$defaultHydrationCacheImpl = $this->_em->getConfiguration()->getHydrationCacheImpl();
if ($defaultHydrationCacheImpl) {
$profile = $profile->setResultCacheDriver($defaultHydrationCacheImpl);
}
}
} elseif (! $profile->getResultCache()) {
$defaultHydrationCacheImpl = $this->_em->getConfiguration()->getHydrationCacheImpl();
if ($defaultHydrationCacheImpl) {
$profile = $profile->setResultCache(CacheAdapter::wrap($defaultHydrationCacheImpl));
}
}

$this->_hydrationCacheProfile = $profile;
Expand All @@ -540,7 +561,7 @@ public function setHydrationCacheProfile(?QueryCacheProfile $profile = null)
}

/**
* @return QueryCacheProfile
* @return QueryCacheProfile|null
*/
public function getHydrationCacheProfile()
{
Expand All @@ -553,13 +574,29 @@ public function getHydrationCacheProfile()
* If no result cache driver is set in the QueryCacheProfile, the default
* result cache driver is used from the configuration.
*
* @return static This query instance.
* @return $this
*/
public function setResultCacheProfile(?QueryCacheProfile $profile = null)
{
if ($profile !== null && ! $profile->getResultCacheDriver()) {
$resultCacheDriver = $this->_em->getConfiguration()->getResultCacheImpl();
$profile = $profile->setResultCacheDriver($resultCacheDriver);
if ($profile === null) {
$this->_queryCacheProfile = null;

return $this;
}

// DBAL < 3.2
if (! method_exists(QueryCacheProfile::class, 'setResultCache')) {
if (! $profile->getResultCacheDriver()) {
$defaultResultCacheDriver = $this->_em->getConfiguration()->getResultCacheImpl();
if ($defaultResultCacheDriver) {
$profile = $profile->setResultCacheDriver($defaultResultCacheDriver);
}
}
} elseif (! $profile->getResultCache()) {
$defaultResultCache = $this->_em->getConfiguration()->getResultCache();
if ($defaultResultCache) {
$profile = $profile->setResultCache($defaultResultCache);
}
}

$this->_queryCacheProfile = $profile;
Expand All @@ -570,9 +607,11 @@ public function setResultCacheProfile(?QueryCacheProfile $profile = null)
/**
* Defines a cache driver to be used for caching result sets and implicitly enables caching.
*
* @deprecated Use {@see setResultCache()} instead.
*
* @param \Doctrine\Common\Cache\Cache|null $resultCacheDriver Cache driver
*
* @return static This query instance.
* @return $this
*
* @throws InvalidResultCacheDriver
*/
Expand All @@ -583,9 +622,38 @@ public function setResultCacheDriver($resultCacheDriver = null)
throw InvalidResultCacheDriver::create();
}

return $this->setResultCache($resultCacheDriver ? CacheAdapter::wrap($resultCacheDriver) : null);
}

/**
* Defines a cache driver to be used for caching result sets and implicitly enables caching.
*
* @return $this
*/
public function setResultCache(?CacheItemPoolInterface $resultCache = null)
{
if ($resultCache === null) {
if ($this->_queryCacheProfile) {
$this->_queryCacheProfile = new QueryCacheProfile($this->_queryCacheProfile->getLifetime(), $this->_queryCacheProfile->getCacheKey());
}

return $this;
}

// DBAL < 3.2
if (! method_exists(QueryCacheProfile::class, 'setResultCache')) {
$resultCacheDriver = DoctrineProvider::wrap($resultCache);

$this->_queryCacheProfile = $this->_queryCacheProfile
? $this->_queryCacheProfile->setResultCacheDriver($resultCacheDriver)
: new QueryCacheProfile(0, null, $resultCacheDriver);

return $this;
}

$this->_queryCacheProfile = $this->_queryCacheProfile
? $this->_queryCacheProfile->setResultCacheDriver($resultCacheDriver)
: new QueryCacheProfile(0, null, $resultCacheDriver);
? $this->_queryCacheProfile->setResultCache($resultCache)
: new QueryCacheProfile(0, null, $resultCache);

return $this;
}
Expand Down Expand Up @@ -1055,9 +1123,9 @@ private function executeIgnoreQueryCache($parameters = null, $hydrationMode = nu
if ($this->_hydrationCacheProfile !== null) {
[$cacheKey, $realCacheKey] = $this->getHydrationCacheId();

$queryCacheProfile = $this->getHydrationCacheProfile();
$cache = $queryCacheProfile->getResultCacheDriver();
$result = $cache->fetch($cacheKey);
$cache = $this->getHydrationCache();
$cacheItem = $cache->getItem($cacheKey);
$result = $cacheItem->isHit() ? $cacheItem->get() : [];

if (isset($result[$realCacheKey])) {
return $result[$realCacheKey];
Expand All @@ -1067,10 +1135,8 @@ private function executeIgnoreQueryCache($parameters = null, $hydrationMode = nu
$result = [];
}

$setCacheEntry = static function ($data) use ($cache, $result, $cacheKey, $realCacheKey, $queryCacheProfile): void {
$result[$realCacheKey] = $data;

$cache->save($cacheKey, $result, $queryCacheProfile->getLifetime());
$setCacheEntry = static function ($data) use ($cache, $result, $cacheItem, $realCacheKey): void {
$cache->save($cacheItem->set($result + [$realCacheKey => $data]));
};
}

Expand All @@ -1090,6 +1156,24 @@ private function executeIgnoreQueryCache($parameters = null, $hydrationMode = nu
return $data;
}

private function getHydrationCache(): CacheItemPoolInterface
{
assert($this->_hydrationCacheProfile !== null);

// Support for DBAL < 3.2
if (! method_exists($this->_hydrationCacheProfile, 'getResultCache')) {
$cacheDriver = $this->_hydrationCacheProfile->getResultCacheDriver();
assert($cacheDriver !== null);

return CacheAdapter::wrap($cacheDriver);
}

$cache = $this->_hydrationCacheProfile->getResultCache();
assert($cache !== null);

return $cache;
}

/**
* Load from second level cache or executes the query and put into cache.
*
Expand Down Expand Up @@ -1168,6 +1252,7 @@ protected function getHydrationCacheId()
$hints['hydrationMode'] = $this->getHydrationMode();

ksort($hints);
assert($queryCacheProfile !== null);

return $queryCacheProfile->generateCacheKeys($sql, $parameters, $hints);
}
Expand All @@ -1177,15 +1262,17 @@ protected function getHydrationCacheId()
* If this is not explicitly set by the developer then a hash is automatically
* generated for you.
*
* @param string $id
* @param string|null $id
*
* @return static This query instance.
* @return $this
*/
public function setResultCacheId($id)
{
$this->_queryCacheProfile = $this->_queryCacheProfile
? $this->_queryCacheProfile->setCacheKey($id)
: new QueryCacheProfile(0, $id, $this->_em->getConfiguration()->getResultCacheImpl());
if (! $this->_queryCacheProfile) {
return $this->setResultCacheProfile(new QueryCacheProfile(0, $id));
}

$this->_queryCacheProfile = $this->_queryCacheProfile->setCacheKey($id);

return $this;
}
Expand All @@ -1195,7 +1282,7 @@ public function setResultCacheId($id)
*
* @deprecated
*
* @return string
* @return string|null
*/
public function getResultCacheId()
{
Expand Down
3 changes: 3 additions & 0 deletions lib/Doctrine/ORM/Cache/Exception/InvalidResultCacheDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Doctrine\ORM\Cache\Exception;

/**
* @deprecated
*/
final class InvalidResultCacheDriver extends CacheException
{
public static function create(): self
Expand Down
16 changes: 13 additions & 3 deletions lib/Doctrine/ORM/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Internal\Hydration\IterableResult;
Expand All @@ -20,6 +21,7 @@
use Doctrine\ORM\Query\ParserResult;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
use Psr\Cache\CacheItemPoolInterface;

use function array_keys;
use function array_values;
Expand All @@ -29,6 +31,7 @@
use function in_array;
use function ksort;
use function md5;
use function method_exists;
use function reset;
use function serialize;
use function sha1;
Expand Down Expand Up @@ -329,13 +332,20 @@ private function evictResultSetCache(
return;
}

$cacheDriver = $this->_queryCacheProfile->getResultCacheDriver();
$statements = (array) $executor->getSqlStatements(); // Type casted since it can either be a string or an array
$cache = method_exists(QueryCacheProfile::class, 'getResultCache')
? $this->_queryCacheProfile->getResultCache()
: $this->_queryCacheProfile->getResultCacheDriver();

assert($cache !== null);

$statements = (array) $executor->getSqlStatements(); // Type casted since it can either be a string or an array

foreach ($statements as $statement) {
$cacheKeys = $this->_queryCacheProfile->generateCacheKeys($statement, $sqlParams, $types, $connectionParams);

$cacheDriver->delete(reset($cacheKeys));
$cache instanceof CacheItemPoolInterface
? $cache->deleteItem(reset($cacheKeys))
: $cache->delete(reset($cacheKeys));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@
use Doctrine\Common\Cache\ClearableCache;
use Doctrine\Common\Cache\FlushableCache;
use Doctrine\Common\Cache\XcacheCache;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Tools\Console\Command\AbstractEntityManagerCommand;
use InvalidArgumentException;
use LogicException;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

use function get_class;
use function method_exists;
use function sprintf;

/**
Expand Down Expand Up @@ -65,21 +68,22 @@ protected function execute(InputInterface $input, OutputInterface $output)
$ui = new SymfonyStyle($input, $output);

$em = $this->getEntityManager($input);
$cacheDriver = $em->getConfiguration()->getResultCacheImpl();
$cache = method_exists(Configuration::class, 'getResultCache') ? $em->getConfiguration()->getResultCache() : null;
$cacheDriver = method_exists(Configuration::class, 'getResultCacheImpl') ? $em->getConfiguration()->getResultCacheImpl() : null;

if (! $cacheDriver) {
if (! $cacheDriver && ! $cache) {
throw new InvalidArgumentException('No Result cache driver is configured on given EntityManager.');
}

if ($cacheDriver instanceof ApcCache) {
throw new LogicException('Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI.');
if ($cacheDriver instanceof ApcCache || $cache instanceof ApcuAdapter) {
throw new LogicException('Cannot clear APCu Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}

if ($cacheDriver instanceof XcacheCache) {
throw new LogicException('Cannot clear XCache Cache from Console, its shared in the Webserver memory and not accessible from the CLI.');
throw new LogicException('Cannot clear XCache Cache from Console, it\'s shared in the Webserver memory and not accessible from the CLI.');
}

if (! ($cacheDriver instanceof ClearableCache)) {
if (! $cache && ! ($cacheDriver instanceof ClearableCache)) {
throw new LogicException(sprintf(
'Can only clear cache when ClearableCache interface is implemented, %s does not implement.',
get_class($cacheDriver)
Expand All @@ -88,10 +92,10 @@ protected function execute(InputInterface $input, OutputInterface $output)

$ui->comment('Clearing <info>all</info> Result cache entries');

$result = $cacheDriver->deleteAll();
$result = $cache ? $cache->clear() : $cacheDriver->deleteAll();
$message = $result ? 'Successfully deleted cache entries.' : 'No cache entries were deleted.';

if ($input->getOption('flush') === true) {
if ($input->getOption('flush') === true && ! $cache) {
if (! ($cacheDriver instanceof FlushableCache)) {
throw new LogicException(sprintf(
'Can only clear cache when FlushableCache interface is implemented, %s does not implement.',
Expand Down
12 changes: 12 additions & 0 deletions phpstan-dbal2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,15 @@ parameters:
- '/Call to an undefined method Doctrine\\DBAL\\Connection::createSchemaManager\(\)\./'
# Class name will change in DBAL 3.
- '/Class Doctrine\\DBAL\\Platforms\\PostgreSqlPlatform referenced with incorrect case: Doctrine\\DBAL\\Platforms\\PostgreSQLPlatform\./'

# Forward compatibility for DBAL 3.2
- '/^Call to an undefined method Doctrine\\.*::[gs]etResultCache\(\)\.$/'
-
message: '/^Parameter #3 \$resultCache of class Doctrine\\DBAL\\Cache\\QueryCacheProfile constructor/'
path: lib/Doctrine/ORM/AbstractQuery.php

# False positive
-
message: '/^Call to an undefined method Doctrine\\Common\\Cache\\Cache::deleteAll\(\)\.$/'
count: 1
path: lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php
Loading

0 comments on commit 996fa77

Please sign in to comment.