Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for PSR-6 result caches #8996

Merged
merged 1 commit into from
Sep 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should happen to this when 3.2.0 is released? Should we change it to whatever the latest 3.1.x is then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We heavily depend on DBAL. I think we should always have a CI job that checks against a dev version of DBAL. When 3.2.0 is released, this job will test against 3.3-dev automatically and I think that's fine.

Copy link
Member

@greg0ire greg0ire Sep 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a syntax we could use to avoid this 2 being hardcoded here? It's a bit misleading. 3@dev maybe?


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));
derrabus marked this conversation as resolved.
Show resolved Hide resolved
}
}

$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