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

Deprecate using doctrine/cache in favour of PSR-6 #144

Merged
merged 4 commits into from
Feb 13, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions UPGRADE-2.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
UPGRADE FROM 2.1 to 2.2
=======================

* Deprecated using doctrine/cache for metadata caching. The `setCacheDriver` and
`getCacheDriver` methods in `Doctrine\Persistence\Mapping\AbstractMetadata`
have been deprecated. Please use `getCache` and `setCache` with a PSR-6
implementation instead. Note that even after switching to PSR-6,
`getCacheDriver` will return a cache instance that wraps the PSR-6 cache.
Note that if you use a custom implementation of doctrine/cache, the library
may not be able to provide a forward compatibility layer. The cache
implementation MUST extend the `Doctrine\Common\Cache\CacheProvider` class.
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"doctrine/annotations": "^1.0",
"doctrine/cache": "^1.0",
"doctrine/collections": "^1.0",
"doctrine/event-manager": "^1.0"
"doctrine/event-manager": "^1.0",
"psr/cache": "^1.0|^2.0|^3.0",
"symfony/cache": "^4.4|^5.0"
Copy link
Member Author

Choose a reason for hiding this comment

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

This is used to provide the forward/compatibility layer. Once we drop that in 3.0, this becomes a dev dependency.

Copy link
Member

@greg0ire greg0ire Dec 12, 2020

Choose a reason for hiding this comment

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

symfony/cache has a dev-dependency on doctrine/cache, so this means a circular dependency (not for us, but for them when they are in dev mode). I think this might be OK though since it's not meant to last, and since symfony seems to be resorting to COMPOSER_ROOT_VERSION in its CI already: https://github.com/symfony/symfony/search?q=COMPOSER_ROOT_VERSION

Still, we might want to check if we don't break their pipeline after releasing this (or merging it? I think they have builds that use untagged versions).

Copy link
Member Author

Choose a reason for hiding this comment

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

How is there a circular dependency? This is doctrine/persistence, not doctrine/cache.

Copy link
Member

Choose a reason for hiding this comment

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

How is there a circular dependency?

symfony/symfony depends on doctrine/persistence and replaces symfony/cache. Anyway, we should be good here. 😃

Copy link
Member

Choose a reason for hiding this comment

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

Right, nevermind my comment, not sure what I meant but I must have been confused.

},
"require-dev": {
"composer/package-versions-deprecated": "^1.11",
Expand Down
78 changes: 70 additions & 8 deletions lib/Doctrine/Persistence/Mapping/AbstractClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@

namespace Doctrine\Persistence\Mapping;

use BadMethodCallException;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\CacheProvider;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Proxy;
use Psr\Cache\CacheItemPoolInterface;
use ReflectionException;
use Symfony\Component\Cache\Adapter\DoctrineAdapter;
use Symfony\Component\Cache\DoctrineProvider;

use function array_combine;
use function array_keys;
use function array_map;
use function array_reverse;
use function array_unshift;
use function explode;
use function sprintf;
use function str_replace;
use function strpos;
use function strrpos;
use function substr;
use function trigger_error;

use const E_USER_DEPRECATED;

/**
* The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
Expand All @@ -28,11 +41,14 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
*
* @var string
*/
protected $cacheSalt = '$CLASSMETADATA';
protected $cacheSalt = '__CLASSMETADATA__';

/** @var Cache|null */
private $cacheDriver;

/** @var CacheItemPoolInterface|null */
private $cache;

/** @var ClassMetadata[] */
private $loadedMetadata = [];

Expand All @@ -45,23 +61,54 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
/**
* Sets the cache driver used by the factory to cache ClassMetadata instances.
*
* @deprecated setCacheDriver was deprecated in doctrine/persistence 2.2 and will be removed in 3.0. Use setCache instead
*
* @return void
*/
public function setCacheDriver(?Cache $cacheDriver = null)
{
@trigger_error(sprintf('%s is deprecated. Use setCache() with a PSR-6 cache instead.', __METHOD__), E_USER_DEPRECATED);

$this->cacheDriver = $cacheDriver;

if ($cacheDriver === null) {
$this->cache = null;

return;
}

if (! $cacheDriver instanceof CacheProvider) {
throw new BadMethodCallException('Cannot convert cache to PSR-6 cache');
}

$this->cache = new DoctrineAdapter($cacheDriver);
}

/**
* Gets the cache driver used by the factory to cache ClassMetadata instances.
*
* @deprecated getCacheDriver was deprecated in doctrine/persistence 2.2 and will be removed in 3.0. Use getCache instead
*
* @return Cache|null
*/
public function getCacheDriver()
{
@trigger_error(sprintf('%s is deprecated. Use getCache() instead.', __METHOD__), E_USER_DEPRECATED);

return $this->cacheDriver;
}

public function setCache(CacheItemPoolInterface $cache): void
{
$this->cache = $cache;
$this->cacheDriver = new DoctrineProvider($cache);
Copy link
Member

Choose a reason for hiding this comment

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

this should use the doctrine/cache PSR-6 provider instead, which is available in the latest version

Copy link
Member Author

Choose a reason for hiding this comment

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

The DoctrineAdapter could be replaced. I've created doctrine/cache#366 to also include this class in doctrine/cache so we no longer have external dependencies for the forward and backward compatibility layers. I'll follow up with a PR here once this is done.

}

public function getCache(): ?CacheItemPoolInterface
{
return $this->cache;
}

/**
* Returns an array of all the loaded metadata currently in memory.
*
Expand Down Expand Up @@ -174,19 +221,29 @@ public function getMetadataFor($className)
$loadingException = null;

try {
if ($this->cacheDriver) {
$cached = $this->cacheDriver->fetch($realClassName . $this->cacheSalt);
if ($this->cache) {
$cached = $this->cache->getItem($this->getCacheKey($realClassName))->get();
if ($cached instanceof ClassMetadata) {
$this->loadedMetadata[$realClassName] = $cached;

$this->wakeupReflection($cached, $this->getReflectionService());
} else {
foreach ($this->loadMetadata($realClassName) as $loadedClassName) {
$this->cacheDriver->save(
$loadedClassName . $this->cacheSalt,
$this->loadedMetadata[$loadedClassName]
);
$loadedMetadata = $this->loadMetadata($realClassName);
$classNames = array_combine(
array_map([$this, 'getCacheKey'], $loadedMetadata),
$loadedMetadata
);

foreach ($this->cache->getItems(array_keys($classNames)) as $item) {
if (! isset($classNames[$item->getKey()])) {
continue;
}

$item->set($this->loadedMetadata[$classNames[$item->getKey()]]);
$this->cache->saveDeferred($item);
}

$this->cache->commit();
}
} else {
$this->loadMetadata($realClassName);
Expand Down Expand Up @@ -400,6 +457,11 @@ public function getReflectionService()
return $this->reflectionService;
}

protected function getCacheKey(string $realClassName): string
{
return str_replace('\\', '__', $realClassName) . $this->cacheSalt;
Copy link
Member Author

Choose a reason for hiding this comment

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

This (along with the changed cache salt) will invalidate existing caches. We'll have to check if existing implementations manually mess with cache keys, in which case we'll have to come back and check the impact of this change.

}

/**
* Gets the real class name of a class name that could be a proxy.
*/
Expand Down
126 changes: 107 additions & 19 deletions tests/Doctrine/Tests/Persistence/Mapping/ClassMetadataFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
namespace Doctrine\Tests\Persistence\Mapping;

use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Mapping\MappingException;
use Doctrine\Tests\DoctrineTestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use stdClass;

use function assert;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\DoctrineAdapter;
use Symfony\Component\Cache\DoctrineProvider;

/**
* @covers \Doctrine\Persistence\Mapping\AbstractClassMetadataFactory
Expand All @@ -27,12 +29,31 @@ protected function setUp(): void
$this->cmf = new TestClassMetadataFactory($driver, $metadata);
}

public function testGetCacheDriver(): void
public function testSetGetCacheDriver(): void
{
self::assertNull($this->cmf->getCacheDriver());
self::assertNull($this->cmf->getCache());

$cache = new ArrayCache();
$this->cmf->setCacheDriver($cache);

self::assertSame($cache, $this->cmf->getCacheDriver());
self::assertInstanceOf(DoctrineAdapter::class, $this->cmf->getCache());

$this->cmf->setCacheDriver(null);
self::assertNull($this->cmf->getCacheDriver());
self::assertNull($this->cmf->getCache());
}

public function testSetGetCache(): void
{
self::assertNull($this->cmf->getCache());
self::assertNull($this->cmf->getCacheDriver());

$cache = new ArrayAdapter();
$this->cmf->setCache($cache);
self::assertSame($cache, $this->cmf->getCache());
self::assertInstanceOf(DoctrineProvider::class, $this->cmf->getCacheDriver());
}

public function testGetMetadataFor(): void
Expand Down Expand Up @@ -61,22 +82,26 @@ public function testGetParentMetadata(): void
public function testGetCachedMetadata(): void
{
$metadata = $this->createMock(ClassMetadata::class);
$cache = new ArrayCache();
$cache->save(ChildEntity::class . '$CLASSMETADATA', $metadata);
$cache = new ArrayAdapter();
$item = $cache->getItem($this->cmf->getCacheKey(ChildEntity::class));
$item->set($metadata);
$cache->save($item);

$this->cmf->setCacheDriver($cache);
$this->cmf->setCache($cache);

self::assertSame($metadata, $this->cmf->getMetadataFor(ChildEntity::class));
self::assertEquals($metadata, $this->cmf->getMetadataFor(ChildEntity::class));
}

public function testCacheGetMetadataFor(): void
{
$cache = new ArrayCache();
$this->cmf->setCacheDriver($cache);
$cache = new ArrayAdapter();
$this->cmf->setCache($cache);

$loadedMetadata = $this->cmf->getMetadataFor(ChildEntity::class);

self::assertSame($loadedMetadata, $cache->fetch(ChildEntity::class . '$CLASSMETADATA'));
$item = $cache->getItem($this->cmf->getCacheKey(ChildEntity::class));
self::assertTrue($item->isHit());
self::assertEquals($loadedMetadata, $item->get());
}

public function testGetAliasedMetadata(): void
Expand Down Expand Up @@ -139,18 +164,81 @@ public function testWillFailOnFallbackFailureWithNotLoadedMetadata(): void
*/
public function testWillIgnoreCacheEntriesThatAreNotMetadataInstances(): void
{
$cacheDriver = $this->createMock(Cache::class);

$this->cmf->setCacheDriver($cacheDriver);

$cacheDriver->expects(self::once())->method('fetch')->with('Foo$CLASSMETADATA')->willReturn(new stdClass());
$key = $this->cmf->getCacheKey(RootEntity::class);

$metadata = $this->cmf->metadata;

$item = $this->createMock(CacheItemInterface::class);

$item
->method('getKey')
->willReturn($key);
$item
->method('get')
->willReturn(new stdClass());
$item
->expects(self::once())
->method('set')
->with($metadata);

$cacheDriver = $this->createMock(CacheItemPoolInterface::class);
$cacheDriver
->method('getItem')
->with($key)
->willReturn($item);
$cacheDriver
->expects(self::once())
->method('getItems')
->with([$key])
->willReturn([$item]);
$cacheDriver
->expects(self::once())
->method('saveDeferred')
->with($item);
$cacheDriver
->expects(self::once())
->method('commit');

$this->cmf->setCache($cacheDriver);

self::assertSame($metadata, $this->cmf->getMetadataFor(RootEntity::class));
}

$metadata = $this->createMock(ClassMetadata::class);
assert($metadata instanceof ClassMetadata);
public function testWillNotCacheFallbackMetadata(): void
Copy link
Member Author

Choose a reason for hiding this comment

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

I duplicated that test, as I realised that using the onMetadataNotFound fallback does not cache that metadata. Since I assume this is intended, I haven't made changes and instead opted to codify that behaviour in tests.

{
$key = $this->cmf->getCacheKey('Foo');

$metadata = $this->cmf->metadata;

$item = $this->createMock(CacheItemInterface::class);

$item
->method('get')
->willReturn(null);
$item
->expects(self::never())
->method('set');

$cacheDriver = $this->createMock(CacheItemPoolInterface::class);
$cacheDriver
->expects(self::once())
->method('getItem')
->with($key)
->willReturn($item);
$cacheDriver
->expects(self::never())
->method('saveDeferred');
$cacheDriver
->expects(self::never())
->method('commit');

$this->cmf->setCache($cacheDriver);

$fallbackCallback = $this->getMockBuilder(stdClass::class)->setMethods(['__invoke'])->getMock();

$fallbackCallback->expects(self::any())->method('__invoke')->willReturn($metadata);
$fallbackCallback
->method('__invoke')
->willReturn($metadata);

$this->cmf->fallbackCallback = $fallbackCallback;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,9 @@ public function isTransient($class): bool

return $class !== $name;
}

public function getCacheKey(string $realClassName): string
{
return parent::getCacheKey($realClassName);
}
}