Merge origin/2.2.x into 3.0.x (using imerge)

This commit is contained in:
Grégoire Paris
2021-05-15 22:00:48 +02:00
47 changed files with 788 additions and 4946 deletions

1
.gitattributes vendored
View File

@@ -7,4 +7,5 @@
/phpunit.xml.dist export-ignore
/phpcs.xml.dist export-ignore
/phpstan.neon export-ignore
/psalm.xml export-ignore
/composer.lock export-ignore

View File

@@ -5,11 +5,9 @@ on:
pull_request:
branches:
- "*.x"
- "master"
push:
branches:
- "*.x"
- "master"
env:
COMPOSER_ROOT_VERSION: "2.1"
@@ -35,15 +33,10 @@ jobs:
php-version: "${{ matrix.php-version }}"
tools: "cs2pr"
- name: "Cache dependencies installed with Composer"
uses: "actions/cache@v2"
with:
path: "~/.composer/cache"
key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
restore-keys: "php-${{ matrix.php-version }}-composer-locked-"
- name: "Install dependencies with Composer"
run: "composer install --no-interaction --no-progress --no-suggest"
uses: "ramsey/composer-install@v1"
with:
dependency-versions: "${{ matrix.deps }}"
# https://github.com/doctrine/.github/issues/3
- name: "Run PHP_CodeSniffer"

View File

@@ -20,7 +20,7 @@ jobs:
- "7.4"
- "8.0"
deps:
- "normal"
- "highest"
include:
- deps: "low"
php-version: "7.2"
@@ -37,31 +37,11 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: "pcov"
- name: "Cache dependencies installed with composer"
uses: "actions/cache@v2"
- name: "Update dependencies with Composer"
uses: "ramsey/composer-install@v1"
with:
path: "~/.composer/cache"
key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
restore-keys: "php-${{ matrix.php-version }}-composer-locked-"
# Remove this block when
# https://github.com/felixfbecker/php-language-server-protocol/pull/15 is
# merged and released
- name: "Remove dependency on vimeo/psalm for PHP8"
run: "composer remove --dev --no-update vimeo/psalm"
if: "${{ matrix.php-version == '8.0' }}"
- name: "Downgrade Composer"
run: "composer self-update --1"
if: "${{ matrix.php-version == '7.1' }}"
- name: "Update dependencies with composer"
run: "composer update --no-interaction --no-progress --no-suggest"
if: "${{ matrix.deps == 'normal' }}"
- name: "Install lowest possible dependencies with composer"
run: "composer update --no-interaction --no-progress --no-suggest --prefer-dist --prefer-lowest"
if: "${{ matrix.deps == 'low' }}"
dependency-versions: "${{ matrix.deps }}"
- name: "Run PHPUnit"
run: "vendor/bin/phpunit --coverage-clover=coverage.xml"

View File

@@ -5,11 +5,9 @@ on:
pull_request:
branches:
- "*.x"
- "master"
push:
branches:
- "*.x"
- "master"
env:
COMPOSER_ROOT_VERSION: "2.1"
@@ -34,15 +32,10 @@ jobs:
php-version: "${{ matrix.php-version }}"
tools: "cs2pr"
- name: "Cache dependencies installed with composer"
uses: "actions/cache@v2"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v1"
with:
path: "~/.composer/cache"
key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}"
restore-keys: "php-${{ matrix.php-version }}-composer-locked-"
- name: "Install dependencies with composer"
run: "composer install --no-interaction --no-progress --no-suggest"
dependency-versions: "${{ matrix.deps }}"
- name: "Run a static analysis with phpstan/phpstan"
run: "vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr"
@@ -57,10 +50,19 @@ jobs:
- "7.4"
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: "Checkout code"
uses: "actions/checkout@v2"
- name: Psalm
uses: docker://vimeo/psalm-github-actions
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
composer_require_dev: true
coverage: "none"
php-version: "${{ matrix.php-version }}"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v1"
with:
dependency-versions: "highest"
- name: "Run a static analysis with vimeo/psalm"
run: "vendor/bin/psalm --show-info=false --stats --output-format=github --threads=$(nproc)"

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
/phpcs.xml
/.phpcs-cache
/.phpunit.result.cache
/composer.lock

11
UPGRADE-2.2.md Normal file
View File

@@ -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.

View File

@@ -22,19 +22,21 @@
"require": {
"php": "^7.2 || ^8.0",
"doctrine/annotations": "^1.7.0",
"doctrine/cache": "^1.0",
"doctrine/cache": "^1.11 || ^2.0",
"doctrine/collections": "^1.0",
"doctrine/event-manager": "^1.0"
"doctrine/event-manager": "^1.0",
"psr/cache": "^1.0|^2.0|^3.0"
},
"require-dev": {
"composer/package-versions-deprecated": "^1.11",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan": "0.12.84",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"doctrine/coding-standard": "^6.0 || ^8.0",
"doctrine/coding-standard": "^6.0 || ^9.0",
"doctrine/common": "^3.0",
"phpunit/phpunit": "^8.0 || ^9.0",
"vimeo/psalm": "^3.11"
"symfony/cache": "^4.4|^5.0",
"vimeo/psalm": "4.7.0"
},
"conflict": {
"doctrine/common": "<2.10@dev"

4677
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,12 +31,16 @@ abstract class AbstractManagerRegistry implements ManagerRegistry
/** @var string */
private $defaultManager;
/** @var string */
/**
* @var string
* @psalm-var class-string
*/
private $proxyInterfaceName;
/**
* @param array<string, string> $connections
* @param array<string, string> $managers
* @psalm-param class-string $proxyInterfaceName
*/
public function __construct(
string $name,
@@ -166,14 +170,9 @@ abstract class AbstractManagerRegistry implements ManagerRegistry
*/
public function getManagerForClass(string $class)
{
// Check for namespace alias
if (strpos($class, ':') !== false) {
[$namespaceAlias, $simpleClassName] = explode(':', $class, 2);
$className = $this->getRealClassName($class);
$class = $this->getAliasNamespace($namespaceAlias) . '\\' . $simpleClassName;
}
$proxyClass = new ReflectionClass($class);
$proxyClass = new ReflectionClass($className);
if ($proxyClass->implementsInterface($this->proxyInterfaceName)) {
$parentClass = $proxyClass->getParentClass();
@@ -182,13 +181,13 @@ abstract class AbstractManagerRegistry implements ManagerRegistry
return null;
}
$class = $parentClass->getName();
$className = $parentClass->getName();
}
foreach ($this->managers as $id) {
$manager = $this->getService($id);
if (! $manager->getMetadataFactory()->isTransient($class)) {
if (! $manager->getMetadataFactory()->isTransient($className)) {
return $manager;
}
}
@@ -264,4 +263,21 @@ abstract class AbstractManagerRegistry implements ManagerRegistry
return $this->getManagerForClass($persistentObject) ?? $this->getManager();
}
/**
* @psalm-return class-string
*/
private function getRealClassName(string $classNameOrAlias): string
{
// Check for namespace alias
if (strpos($classNameOrAlias, ':') !== false) {
[$namespaceAlias, $simpleClassName] = explode(':', $classNameOrAlias, 2);
/** @psalm-var class-string */
return $this->getAliasNamespace($namespaceAlias) . '\\' . $simpleClassName;
}
/** @psalm-var class-string */
return $classNameOrAlias;
}
}

View File

@@ -13,12 +13,15 @@ use Doctrine\Persistence\ObjectManager;
*/
class LoadClassMetadataEventArgs extends EventArgs
{
/** @var ClassMetadata */
/** @psalm-var ClassMetadata<object> */
private $classMetadata;
/** @var ObjectManager */
private $objectManager;
/**
* @psalm-param ClassMetadata<object> $classMetadata
*/
public function __construct(ClassMetadata $classMetadata, ObjectManager $objectManager)
{
$this->classMetadata = $classMetadata;
@@ -28,7 +31,7 @@ class LoadClassMetadataEventArgs extends EventArgs
/**
* Retrieves the associated ClassMetadata.
*
* @return ClassMetadata
* @psalm-return ClassMetadata<object>
*/
public function getClassMetadata()
{

View File

@@ -58,7 +58,7 @@ interface ManagerRegistry extends ConnectionRegistry
*
* @param string $alias The alias.
*
* @phpstan-return class-string The full namespace.
* @phpstan-return string The full namespace.
*/
public function getAliasNamespace(string $alias);
@@ -74,12 +74,12 @@ interface ManagerRegistry extends ConnectionRegistry
*
* @param string $persistentObject The name of the persistent object.
* @param string $persistentManagerName The object manager name (null for the default one).
* @psalm-param class-string<T> $persistentObject
*
* @return ObjectRepository
* @psalm-return ObjectRepository<T>
*
* @template T of object
* @psalm-param class-string<T> $persistentObject
* @psalm-return ObjectRepository<T>
*/
public function getRepository(
string $persistentObject,

View File

@@ -4,17 +4,32 @@ declare(strict_types=1);
namespace Doctrine\Persistence\Mapping;
use BadMethodCallException;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\CacheProvider;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Proxy;
use Psr\Cache\CacheItemPoolInterface;
use ReflectionException;
use function array_combine;
use function array_keys;
use function array_map;
use function array_reverse;
use function array_unshift;
use function assert;
use function explode;
use function is_array;
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
@@ -22,6 +37,9 @@ use function substr;
* to a relational database.
*
* This class was abstracted from the ORM ClassMetadataFactory.
*
* @template CMTemplate of ClassMetadata
* @template-implements ClassMetadataFactory<CMTemplate>
*/
abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
{
@@ -30,12 +48,18 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
*
* @var string
*/
protected $cacheSalt = '$CLASSMETADATA';
protected $cacheSalt = '__CLASSMETADATA__';
/** @var Cache|null */
private $cacheDriver;
/** @var array<string, ClassMetadata> */
/** @var CacheItemPoolInterface|null */
private $cache;
/**
* @var array<string, ClassMetadata>
* @psalm-var CMTemplate[]
*/
private $loadedMetadata = [];
/** @var bool */
@@ -44,30 +68,65 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
/** @var ReflectionService|null */
private $reflectionService = null;
/** @var ProxyClassNameResolver|null */
private $proxyClassNameResolver = null;
/**
* 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 = CacheAdapter::wrap($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.
*
* @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 = DoctrineProvider::wrap($cache);
}
final protected function getCache(): ?CacheItemPoolInterface
{
return $this->cache;
}
/**
* Returns an array of all the loaded metadata currently in memory.
*
* @return ClassMetadata[]
* @psalm-return CMTemplate[]
*/
public function getLoadedMetadata()
{
@@ -75,10 +134,7 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
}
/**
* Forces the factory to load the metadata of all classes known to the underlying
* mapping driver.
*
* @return array<int, ClassMetadata> The ClassMetadata instances of all mapped classes.
* {@inheritDoc}
*/
public function getAllMetadata()
{
@@ -95,6 +151,11 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
return $metadata;
}
public function setProxyClassNameResolver(ProxyClassNameResolver $resolver): void
{
$this->proxyClassNameResolver = $resolver;
}
/**
* Lazy initialization of this stuff, especially the metadata driver,
* since these are not needed at all when a metadata cache is active.
@@ -107,7 +168,6 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
* Gets the fully qualified class-name from the namespace alias.
*
* @return string
*
* @psalm-return class-string
*/
abstract protected function getFqcnFromAlias(
@@ -125,6 +185,8 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
/**
* Wakes up reflection after ClassMetadata gets unserialized from cache.
*
* @psalm-param CMTemplate $class
*
* @return void
*/
abstract protected function wakeupReflection(
@@ -135,6 +197,8 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
/**
* Initializes Reflection after ClassMetadata was constructed.
*
* @psalm-param CMTemplate $class
*
* @return void
*/
abstract protected function initializeReflection(
@@ -147,16 +211,14 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
*
* This method should return false for mapped superclasses or embedded classes.
*
* @psalm-param CMTemplate $class
*
* @return bool
*/
abstract protected function isEntity(ClassMetadata $class);
/**
* Gets the class metadata descriptor for a class.
*
* @param string $className The name of the class.
*
* @return ClassMetadata
* {@inheritDoc}
*
* @throws ReflectionException
* @throws MappingException
@@ -173,6 +235,7 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
$realClassName = $this->getFqcnFromAlias($namespaceAlias, $simpleClassName);
} else {
/** @psalm-var class-string $className */
$realClassName = $this->getRealClass($className);
}
@@ -184,20 +247,31 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
$loadingException = null;
try {
if ($this->cacheDriver !== null) {
$cached = $this->cacheDriver->fetch($realClassName . $this->cacheSalt);
if ($this->cache !== null) {
$cached = $this->cache->getItem($this->getCacheKey($realClassName))->get();
if ($cached instanceof ClassMetadata) {
/** @psalm-var CMTemplate $cached */
$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
);
assert(is_array($classNames));
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);
@@ -221,9 +295,7 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
}
/**
* Checks whether the factory has the metadata for a class loaded already.
*
* @return bool TRUE if the metadata of the class in question is already loaded, FALSE otherwise.
* {@inheritDoc}
*/
public function hasMetadataFor(string $className)
{
@@ -235,6 +307,8 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
*
* NOTE: This is only useful in very special cases, like when generating proxy classes.
*
* @psalm-param CMTemplate $class
*
* @return void
*/
public function setMetadataFor(string $className, ClassMetadata $class)
@@ -245,9 +319,10 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
/**
* Gets an array of parent classes for the given entity class.
*
* @return array<int, string>
*
* @psalm-param class-string $name
*
* @return string[]
* @psalm-return class-string[]
*/
protected function getParentClasses(string $name)
{
@@ -279,10 +354,9 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
* should be used for reflection.
*
* @param string $name The name of the class for which the metadata should get loaded.
* @psalm-param class-string $name
*
* @return array<int, string>
*
* @psalm-param class-string $name
*/
protected function loadMetadata(string $name)
{
@@ -343,6 +417,7 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
* Override this method to implement a fallback strategy for failed metadata loading
*
* @return ClassMetadata|null
* @psalm-return CMTemplate|null
*/
protected function onNotFoundMetadata(string $className)
{
@@ -352,7 +427,10 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
/**
* Actually loads the metadata from the underlying metadata.
*
* @param array<int, string> $nonSuperclassParents All parent class names that are not marked as mapped superclasses.
* @param string[] $nonSuperclassParents All parent class names that are
* not marked as mapped superclasses.
* @psalm-param CMTemplate $class
* @psalm-param CMTemplate|null $parent
*
* @return void
*/
@@ -366,12 +444,19 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
/**
* Creates a new ClassMetadata instance for the given class name.
*
* @return ClassMetadata
* @psalm-param class-string<T> $className
*
* @return ClassMetadata<T>
* @psalm-return CMTemplate
*
* @template T of object
*/
abstract protected function newClassMetadataInstance(string $className);
/**
* {@inheritDoc}
*
* @psalm-param class-string|string $class
*/
public function isTransient(string $class)
{
@@ -386,6 +471,7 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
$class = $this->getFqcnFromAlias($namespaceAlias, $simpleClassName);
}
/** @psalm-var class-string $class */
return $this->getDriver()->isTransient($class);
}
@@ -413,20 +499,53 @@ abstract class AbstractClassMetadataFactory implements ClassMetadataFactory
return $this->reflectionService;
}
protected function getCacheKey(string $realClassName): string
{
return str_replace('\\', '__', $realClassName) . $this->cacheSalt;
}
/**
* Gets the real class name of a class name that could be a proxy.
*
* @psalm-param class-string $class
* @psalm-return class-string
* @psalm-param class-string<Proxy<T>>|class-string<T> $class
*
* @psalm-return class-string<T>
*
* @template T of object
*/
private function getRealClass(string $class): string
{
$pos = strrpos($class, '\\' . Proxy::MARKER . '\\');
if ($pos === false) {
return $class;
if ($this->proxyClassNameResolver === null) {
$this->createDefaultProxyClassNameResolver();
}
return substr($class, $pos + Proxy::MARKER_LENGTH + 2);
assert($this->proxyClassNameResolver !== null);
return $this->proxyClassNameResolver->resolveClassName($class);
}
private function createDefaultProxyClassNameResolver(): void
{
$this->proxyClassNameResolver = new class implements ProxyClassNameResolver {
/**
* @psalm-param class-string<Proxy<T>>|class-string<T> $className
*
* @psalm-return class-string<T>
*
* @template T of object
*/
public function resolveClassName(string $className): string
{
$pos = strrpos($className, '\\' . Proxy::MARKER . '\\');
if ($pos === false) {
/** @psalm-var class-string<T> */
return $className;
}
/** @psalm-var class-string<T> */
return substr($className, $pos + Proxy::MARKER_LENGTH + 2);
}
};
}
}

View File

@@ -8,6 +8,8 @@ use ReflectionClass;
/**
* Contract for a Doctrine persistence layer ClassMetadata class to implement.
*
* @template-covariant T of object
*/
interface ClassMetadata
{
@@ -15,6 +17,7 @@ interface ClassMetadata
* Gets the fully-qualified class name of this persistent class.
*
* @return string
* @psalm-return class-string<T>
*/
public function getName();
@@ -30,7 +33,7 @@ interface ClassMetadata
/**
* Gets the ReflectionClass instance for this mapped class.
*
* @return ReflectionClass<object>
* @return ReflectionClass<T>
*/
public function getReflectionClass();
@@ -108,6 +111,7 @@ interface ClassMetadata
* Returns the target class name of the given association.
*
* @return string
* @psalm-return class-string
*/
public function getAssociationTargetClass(string $assocName);

View File

@@ -6,6 +6,8 @@ namespace Doctrine\Persistence\Mapping;
/**
* Contract for a Doctrine persistence layer ClassMetadata class to implement.
*
* @template T of ClassMetadata
*/
interface ClassMetadataFactory
{
@@ -13,7 +15,8 @@ interface ClassMetadataFactory
* Forces the factory to load the metadata of all classes known to the underlying
* mapping driver.
*
* @return array<int, ClassMetadata> The ClassMetadata instances of all mapped classes.
* @return ClassMetadata[] The ClassMetadata instances of all mapped classes.
* @psalm-return T[]
*/
public function getAllMetadata();
@@ -23,6 +26,7 @@ interface ClassMetadataFactory
* @param string $className The name of the class.
*
* @return ClassMetadata
* @psalm-return T
*/
public function getMetadataFor(string $className);
@@ -36,6 +40,8 @@ interface ClassMetadataFactory
/**
* Sets the metadata descriptor for a specific class.
*
* @psalm-param T $class
*
* @return void
*/
public function setMetadataFor(string $className, ClassMetadata $class);
@@ -44,6 +50,8 @@ interface ClassMetadataFactory
* Returns whether the class with the specified name should have its metadata loaded.
* This is only the case if it is either mapped directly or as a MappedSuperclass.
*
* @psalm-param class-string $className
*
* @return bool
*/
public function isTransient(string $className);

View File

@@ -71,7 +71,7 @@ abstract class AnnotationDriver implements MappingDriver
/**
* Name of the entity annotations as keys.
*
* @var array<string, int>
* @var array<class-string, bool|int>
*/
protected $entityAnnotationClasses = [];
@@ -122,7 +122,7 @@ abstract class AnnotationDriver implements MappingDriver
/**
* Append exclude lookup paths to metadata driver.
*
* @param array<int, string> $paths
* @param string[] $paths
*
* @return void
*/
@@ -181,6 +181,8 @@ abstract class AnnotationDriver implements MappingDriver
*
* A class is non-transient if it is annotated with an annotation
* from the {@see AnnotationDriver::entityAnnotationClasses}.
*
* {@inheritDoc}
*/
public function isTransient(string $className)
{

View File

@@ -26,7 +26,7 @@ abstract class FileDriver implements MappingDriver
/** @var FileLocator */
protected $locator;
/** @var ClassMetadata[]|null */
/** @psalm-var ClassMetadata<object>[]|null */
protected $classCache;
/** @var string */
@@ -72,7 +72,7 @@ abstract class FileDriver implements MappingDriver
* Gets the element of schema meta data for the class from the mapping file.
* This will lazily load the mapping file if it is not loaded yet.
*
* @return ClassMetadata The element of schema meta data.
* @psalm-return ClassMetadata<object> The element of schema meta data.
*
* @throws MappingException
*/
@@ -129,7 +129,7 @@ abstract class FileDriver implements MappingDriver
return $this->locator->getAllClassNames($this->globalBasename);
}
/** @var array<string, ClassMetadata> $classCache */
/** @var array<string, ClassMetadata<object>> $classCache */
$classCache = $this->classCache;
/** @var array<int, string> $keys */
@@ -148,6 +148,7 @@ abstract class FileDriver implements MappingDriver
* @param string $file The mapping file to load.
*
* @return ClassMetadata[]
* @psalm-return array<class-string, ClassMetadata<object>>
*/
abstract protected function loadMappingFile(string $file);

View File

@@ -14,6 +14,8 @@ interface MappingDriver
/**
* Loads the metadata for the specified class into the provided container.
*
* @param ClassMetadata<object> $metadata
*
* @return void
*/
public function loadMetadataForClass(string $className, ClassMetadata $metadata);
@@ -29,9 +31,9 @@ interface MappingDriver
* Returns whether the class with the specified name should have its metadata loaded.
* This is only the case if it is either mapped as an Entity or a MappedSuperclass.
*
* @return bool
*
* @psalm-param class-string $className
*
* @return bool
*/
public function isTransient(string $className);
}

View File

@@ -12,7 +12,7 @@ use Doctrine\Persistence\Mapping\ClassMetadata;
*/
class PHPDriver extends FileDriver
{
/** @var ClassMetadata */
/** @var ClassMetadata<object> */
protected $metadata;
/**

View File

@@ -52,7 +52,7 @@ class MappingException extends Exception
return new self(sprintf(
'File mapping drivers must have a valid directory path, ' .
'however the given path %s seems to be incorrect!',
$path
(string) $path
));
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping;
use Doctrine\Persistence\Proxy;
interface ProxyClassNameResolver
{
/**
* @psalm-param class-string<Proxy<T>>|class-string<T> $className
*
* @psalm-return class-string<T>
*
* @template T of object
*/
public function resolveClassName(string $className): string;
}

View File

@@ -18,56 +18,57 @@ interface ReflectionService
/**
* Returns an array of the parent classes (not interfaces) for the given class.
*
* @psalm-param class-string $class
*
* @return string[]
* @psalm-return class-string[]
*
* @throws MappingException
*
* @psalm-param class-string $class
* @psalm-return class-string[]
*/
public function getParentClasses(string $class);
/**
* Returns the shortname of a class.
*
* @return string
*
* @psalm-param class-string $class
*
* @return string
*/
public function getClassShortName(string $class);
/**
* @return string
*
* @psalm-param class-string $class
*
* @return string
*/
public function getClassNamespace(string $class);
/**
* Returns a reflection class instance or null.
*
* @psalm-param class-string<T> $class
*
* @return ReflectionClass<T>|null
*
* @template T of object
* @psalm-param class-string<T> $class
*/
public function getClass(string $class);
/**
* Returns an accessible property (setAccessible(true)) or null.
*
* @return ReflectionProperty|null
*
* @psalm-param class-string $class
*
* @return ReflectionProperty|null
*/
public function getAccessibleProperty(string $class, string $property);
/**
* Checks if the class have a public method with the given name.
*
* @return bool
*
* @psalm-param class-string $class
*
* @return bool
*/
public function hasPublicMethod(string $class, string $method);
}

View File

@@ -12,6 +12,7 @@ use ReflectionMethod;
use ReflectionProperty;
use function array_key_exists;
use function assert;
use function class_exists;
use function class_parents;
use function phpversion;
@@ -39,7 +40,11 @@ class RuntimeReflectionService implements ReflectionService
throw MappingException::nonExistingClass($class);
}
return class_parents($class);
$parents = class_parents($class);
assert($parents !== false);
return $parents;
}
/**
@@ -63,7 +68,11 @@ class RuntimeReflectionService implements ReflectionService
}
/**
* {@inheritDoc}
* @psalm-param class-string<T> $class
*
* @return ReflectionClass<T>
*
* @template T of object
*/
public function getClass(string $class)
{

View File

@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace Doctrine\Persistence\Mapping;
use function assert;
use function is_int;
use function strpos;
use function strrev;
use function strrpos;
@@ -29,11 +27,10 @@ class StaticReflectionService implements ReflectionService
*/
public function getClassShortName(string $className)
{
if (strpos($className, '\\') !== false) {
$pos = strrpos($className, '\\');
assert(is_int($pos));
$nsSeparatorLastPosition = strrpos($className, '\\');
$className = substr($className, $pos + 1);
if ($nsSeparatorLastPosition !== false) {
$className = substr($className, $nsSeparatorLastPosition + 1);
}
return $className;
@@ -47,10 +44,7 @@ class StaticReflectionService implements ReflectionService
$namespace = '';
if (strpos($className, '\\') !== false) {
$pos = strpos(strrev($className), '\\');
assert(is_int($pos));
$namespace = strrev(substr(strrev($className), $pos + 1));
$namespace = strrev(substr(strrev($className), (int) strpos(strrev($className), '\\') + 1));
}
return $namespace;

View File

@@ -19,12 +19,12 @@ interface ObjectManager
*
* @param string $className The class name of the object to find.
* @param mixed $id The identity of the object to find.
* @psalm-param class-string<T> $className
*
* @return object|null The found object.
* @psalm-return T|null
*
* @template T of object
* @psalm-param class-string<T> $className
* @psalm-return T|null
*/
public function find(string $className, $id): ?object;
@@ -100,9 +100,11 @@ interface ObjectManager
/**
* Gets the repository for a class.
*
* @template T of object
* @psalm-param class-string<T> $className
*
* @psalm-return ObjectRepository<T>
*
* @template T of object
*/
public function getRepository(string $className): ObjectRepository;
@@ -111,11 +113,19 @@ interface ObjectManager
*
* The class name must be the fully-qualified class name without a leading backslash
* (as it is returned by get_class($obj)).
*
* @psalm-param class-string<T> $className
*
* @psalm-return ClassMetadata<T>
*
* @template T of object
*/
public function getClassMetadata(string $className): ClassMetadata;
/**
* Gets the metadata factory used to gather the metadata of classes.
*
* @psalm-return ClassMetadataFactory<ClassMetadata<object>>
*/
public function getMetadataFactory(): ClassMetadataFactory;

View File

@@ -24,6 +24,8 @@ interface ObjectManagerAware
{
/**
* Injects responsible ObjectManager and the ClassMetadata into this persistent object.
*
* @psalm-param ClassMetadata<object> $classMetadata
*/
public function injectObjectManager(
ObjectManager $objectManager,

View File

@@ -68,6 +68,9 @@ abstract class ObjectManagerDecorator implements ObjectManager
return $this->wrapped->getClassMetadata($className);
}
/**
* @psalm-return ClassMetadataFactory<ClassMetadata<object>>
*/
public function getMetadataFactory(): ClassMetadataFactory
{
return $this->wrapped->getMetadataFactory();

View File

@@ -19,7 +19,6 @@ interface ObjectRepository
* @param mixed $id The identifier.
*
* @return object|null The object.
*
* @psalm-return T|null
*/
public function find($id): ?object;
@@ -28,7 +27,6 @@ interface ObjectRepository
* Finds all objects in the repository.
*
* @return array<int, object> The objects.
*
* @psalm-return T[]
*/
public function findAll(): array;
@@ -42,12 +40,12 @@ interface ObjectRepository
*
* @param array<string, mixed> $criteria
* @param array<string, string> $orderBy
* @psalm-param array<string, 'asc'|'desc'|'ASC'|'DESC'> $orderBy
*
* @return array<int, object> The objects.
* @psalm-return T[]
*
* @throws UnexpectedValueException
*
* @psalm-return T[]
*/
public function findBy(
array $criteria,
@@ -62,7 +60,6 @@ interface ObjectRepository
* @param array<string, mixed> $criteria The criteria.
*
* @return object|null The object.
*
* @psalm-return T|null
*/
public function findOneBy(array $criteria): ?object;

View File

@@ -6,6 +6,8 @@ namespace Doctrine\Persistence;
/**
* Interface for proxy classes.
*
* @template T of object
*/
interface Proxy
{

View File

@@ -4,8 +4,11 @@ declare(strict_types=1);
namespace Doctrine\Persistence\Reflection;
use Closure;
use ReflectionProperty;
use function assert;
/**
* PHP Typed No Default Reflection Property - special override for typed properties without a default value.
*/
@@ -42,6 +45,7 @@ class TypedNoDefaultReflectionProperty extends ReflectionProperty
unset($this->$propertyName);
};
$unsetter = $unsetter->bindTo($object, $this->getDeclaringClass()->getName());
$unsetter();
return;

View File

@@ -48,12 +48,4 @@
<rule ref="SlevomatCodingStandard.Classes.SuperfluousExceptionNaming.SuperfluousSuffix">
<exclude-pattern>lib/Doctrine/Persistence/Mapping/MappingException.php</exclude-pattern>
</rule>
<rule ref="SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedProperty">
<exclude-pattern>RuntimeReflectionServiceTest</exclude-pattern>
</rule>
<rule ref="SlevomatCodingStandard.Classes.UnusedPrivateElements.WriteOnlyProperty">
<exclude-pattern>tests/Doctrine/Tests_PHP74/Persistence/Reflection/TypedNoDefaultReflectionPropertyTest.php</exclude-pattern>
</rule>
</ruleset>

View File

@@ -20,29 +20,16 @@ parameters:
-
message: '#Parameter \#1 \$class of method Doctrine\\Persistence\\Mapping\\RuntimeReflectionService\:\:getParentClasses\(\) expects class\-string, string given\.#'
path: 'tests/Doctrine/Tests/Persistence/Mapping/RuntimeReflectionServiceTest.php'
-
message: '#Parameter \#1 \$className of method Doctrine\\Persistence\\Mapping\\Driver\\MappingDriverChain\:\:isTransient\(\) expects class\-string, string given\.#'
path: 'tests/Doctrine/Tests/Persistence/Mapping/DriverChainTest.php'
-
message: '#Parameter \#1 \$className of method Doctrine\\Persistence\\Mapping\\Driver\\FileDriver\:\:isTransient\(\) expects class\-string, string given\.#'
path: 'tests/Doctrine/Tests/Persistence/Mapping/FileDriverTest.php'
-
message: '#Method Doctrine\\Tests\\Persistence\\Mapping\\TestClassMetadataFactory\:\:getFqcnFromAlias\(\) should return class\-string but returns string\.#'
path: 'tests/Doctrine/Tests/Persistence/Mapping/TestClassMetadataFactory.php'
- "#^Instantiated class Doctrine\\\\Common\\\\Cache\\\\ArrayCache not found\\.$#"
-
message: '#Method Doctrine\\Tests\\Persistence\\TestManagerRegistry\:\:getAliasNamespace\(\) should return class\-string but returns string\.#'
path: 'tests/Doctrine/Tests/Persistence/ManagerRegistryTest.php'
-
message: '#Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory\:\:getRealClass\(\) should return class\-string but returns string\.#'
path: 'lib/Doctrine/Persistence/Mapping/AbstractClassMetadataFactory.php'
-
message: '#Parameter \#1 \$class of method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory\:\:getRealClass\(\) expects class\-string, string given\.#'
path: 'lib/Doctrine/Persistence/Mapping/AbstractClassMetadataFactory.php'
-
message: '#Parameter \#1 \$className of method Doctrine\\Persistence\\Mapping\\Driver\\MappingDriver\:\:isTransient\(\) expects class\-string, string given\.#'
path: 'lib/Doctrine/Persistence/Mapping/AbstractClassMetadataFactory.php'
-
message: '#Parameter \#1 \$argument of class ReflectionClass constructor expects class\-string\<T of object\>\|T of object, string given\.#'
message: '#Method Doctrine\\Persistence\\AbstractManagerRegistry\:\:getRealClassName\(\) should return class\-string but returns string\.#'
path: 'lib/Doctrine/Persistence/AbstractManagerRegistry.php'
-
message: '#Call to static method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with Symfony\\Component\\Cache\\Adapter\\ArrayAdapter and null will always evaluate to false\.#'
path: 'tests/Doctrine/Tests/Persistence/Mapping/ClassMetadataFactoryTest.php'

View File

@@ -1,16 +1,17 @@
<?xml version="1.0"?>
<psalm
totallyTyped="false"
errorLevel="4"
resolveFromConfigFile="true"
errorLevel="3"
findUnusedPsalmSuppress="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="lib/Doctrine/Persistence" />
<directory name="tests/Doctrine" />
<ignoreFiles>
<directory name="vendor" />
<file name="tests/Doctrine/Tests/Persistence/Mapping/_files/TestEntity.php" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
@@ -20,5 +21,40 @@
<file name="lib/Doctrine/Persistence/Reflection/TypedNoDefaultReflectionProperty.php"/>
</errorLevel>
</RedundantCondition>
<InvalidNullableReturnType>
<errorLevel type="suppress">
<!-- see https://github.com/vimeo/psalm/issues/5193 -->
<file name="lib/Doctrine/Persistence/Mapping/AbstractClassMetadataFactory.php"/>
</errorLevel>
</InvalidNullableReturnType>
<NullableReturnStatement>
<errorLevel type="suppress">
<!-- see https://github.com/vimeo/psalm/issues/5193 -->
<file name="lib/Doctrine/Persistence/Mapping/AbstractClassMetadataFactory.php"/>
</errorLevel>
</NullableReturnStatement>
<NullArgument>
<errorLevel type="suppress">
<file name="tests/Doctrine/Tests/Persistence/Mapping/SymfonyFileLocatorTest.php"/>
</errorLevel>
</NullArgument>
<UndefinedClass>
<errorLevel type="suppress">
<!-- This test references ArrayCache which has been removed from Doctrine Cache -->
<file name="tests/Doctrine/Tests/Persistence/Mapping/ClassMetadataFactoryTest.php"/>
</errorLevel>
</UndefinedClass>
<ArgumentTypeCoercion>
<errorLevel type="suppress">
<!-- On purpose to use a non existing class for tests -->
<file name="tests/Doctrine/Tests/Persistence/Mapping/RuntimeReflectionServiceTest.php"/>
</errorLevel>
</ArgumentTypeCoercion>
<MoreSpecificReturnType>
<errorLevel type="suppress">
<!-- FileDriver::loadMappingFile() in tests could have a more specific return, but is not needed -->
<file name="tests/Doctrine/Tests/Persistence/Mapping/FileDriverTest.php"/>
</errorLevel>
</MoreSpecificReturnType>
</issueHandlers>
</psalm>

View File

@@ -205,6 +205,8 @@ class TestManagerRegistry extends AbstractManagerRegistry
/**
* {@inheritDoc}
*
* @psalm-param class-string $proxyInterfaceName
*/
public function __construct(
string $name,

View File

@@ -88,8 +88,8 @@ class AnnotationDriverTest extends TestCase
class SimpleAnnotationDriver extends AnnotationDriver
{
/** @var array<string, int> */
protected $entityAnnotationClasses = [Entity::class => 1];
/** @var array<class-string, bool|int> */
protected $entityAnnotationClasses = [Entity::class => true];
public function loadMetadataForClass(string $className, ClassMetadata $metadata): void
{

View File

@@ -6,41 +6,63 @@ namespace Doctrine\Tests\Persistence\Mapping;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
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 ReflectionMethod;
use stdClass;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use function assert;
use function class_exists;
/**
* @covers \Doctrine\Persistence\Mapping\AbstractClassMetadataFactory
*/
class ClassMetadataFactoryTest extends DoctrineTestCase
{
/** @var TestClassMetadataFactory */
/** @var TestClassMetadataFactory<ClassMetadata<object>> */
private $cmf;
protected function setUp(): void
{
$driver = $this->createMock(MappingDriver::class);
/** @psalm-var ClassMetadata<object> */
$metadata = $this->createMock(ClassMetadata::class);
$this->cmf = new TestClassMetadataFactory($driver, $metadata);
}
public function testGetCacheDriver(): void
public function testSetGetCacheDriver(): void
{
self::assertNull($this->cmf->getCacheDriver());
self::assertNull(self::getCache($this->cmf));
$cache = new ArrayCache();
$cache = $this->getArrayCache();
$this->cmf->setCacheDriver($cache);
$cacheDriver = $this->cmf->getCacheDriver();
assert($cacheDriver instanceof ArrayCache);
self::assertSame($cache, $this->cmf->getCacheDriver());
self::assertInstanceOf(CacheItemPoolInterface::class, self::getCache($this->cmf));
self::assertSame($cache, $cacheDriver);
$this->cmf->setCacheDriver(null);
self::assertNull($this->cmf->getCacheDriver());
self::assertNull(self::getCache($this->cmf));
}
public function testSetGetCache(): void
{
self::assertNull(self::getCache($this->cmf));
self::assertNull($this->cmf->getCacheDriver());
$cache = new ArrayAdapter();
$this->cmf->setCache($cache);
self::assertSame($cache, self::getCache($this->cmf));
self::assertInstanceOf(DoctrineProvider::class, $this->cmf->getCacheDriver());
}
public function testGetMetadataFor(): void
@@ -67,22 +89,26 @@ class ClassMetadataFactoryTest extends DoctrineTestCase
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
@@ -122,9 +148,7 @@ class ClassMetadataFactoryTest extends DoctrineTestCase
return $classMetadata;
};
$fooClassMetadata = $this->cmf->getMetadataFor('Foo');
self::assertSame($classMetadata, $fooClassMetadata);
self::assertSame($classMetadata, $this->cmf->getMetadataFor('Foo'));
}
public function testWillFailOnFallbackFailureWithNotLoadedMetadata(): void
@@ -144,33 +168,103 @@ class ClassMetadataFactoryTest extends DoctrineTestCase
*/
public function testWillIgnoreCacheEntriesThatAreNotMetadataInstances(): void
{
$cacheDriver = $this->createMock(Cache::class);
$key = $this->cmf->getCacheKey(RootEntity::class);
$this->cmf->setCacheDriver($cacheDriver);
$metadata = $this->cmf->metadata;
$cacheDriver->expects(self::once())->method('fetch')->with('Foo$CLASSMETADATA')->willReturn(new stdClass());
$item = $this->createMock(CacheItemInterface::class);
$metadata = $this->createMock(ClassMetadata::class);
$item
->method('getKey')
->willReturn($key);
$item
->method('get')
->willReturn(new stdClass());
$item
->expects(self::once())
->method('set')
->with($metadata);
$fallbackCallback = new class ($metadata) {
/** @var ClassMetadata */
private $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');
public function __construct(ClassMetadata $metadata)
{
$this->metadata = $metadata;
}
$this->cmf->setCache($cacheDriver);
public function __invoke(): ClassMetadata
{
return $this->metadata;
}
self::assertSame($metadata, $this->cmf->getMetadataFor(RootEntity::class));
}
public function testWillNotCacheFallbackMetadata(): void
{
$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);
$this->cmf->fallbackCallback = static function () use ($metadata): ClassMetadata {
return $metadata;
};
$this->cmf->fallbackCallback = $fallbackCallback;
self::assertSame($metadata, $this->cmf->getMetadataFor('Foo'));
}
/**
* @psalm-param AbstractClassMetadataFactory<ClassMetadata<object>> $classMetadataFactory
*/
private static function getCache(AbstractClassMetadataFactory $classMetadataFactory): ?CacheItemPoolInterface
{
$method = new ReflectionMethod($classMetadataFactory, 'getCache');
$method->setAccessible(true);
return $method->invoke($classMetadataFactory);
}
private function getArrayCache(): Cache
{
$cache = class_exists(DoctrineProvider::class)
? DoctrineProvider::wrap(new ArrayAdapter())
: new ArrayCache();
assert($cache instanceof Cache);
return $cache;
}
}
class RootEntity

View File

@@ -9,6 +9,9 @@ use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
use Doctrine\Persistence\Mapping\MappingException;
use Doctrine\Tests\DoctrineTestCase;
use Doctrine\Tests\Persistence\Mapping\Fixtures\Manager\Manager;
use Doctrine\Tests\Persistence\Mapping\Fixtures\Model;
use stdClass;
class DriverChainTest extends DoctrineTestCase
{
@@ -26,13 +29,13 @@ class DriverChainTest extends DoctrineTestCase
->method('isTransient');
$driver2 = $this->createMock(MappingDriver::class);
$driver2->expects(self::at(0))
$driver2->expects(self::once())
->method('loadMetadataForClass')
->with(self::equalTo($className), self::equalTo($classMetadata));
$driver2->expects(self::at(1))
$driver2->expects(self::once())
->method('isTransient')
->with(self::equalTo($className))
->will(self::returnValue(true));
->willReturn(true);
$chain->addDriver($driver1, 'Doctrine\Tests\Models\Company');
$chain->addDriver($driver2, 'Doctrine\Tests\Persistence\Mapping');
@@ -86,7 +89,7 @@ class DriverChainTest extends DoctrineTestCase
$chain = new MappingDriverChain();
$chain->addDriver($driver1, 'Doctrine\Tests\Models\CMS');
self::assertTrue($chain->isTransient('stdClass'), 'stdClass isTransient');
self::assertTrue($chain->isTransient(stdClass::class), 'stdClass isTransient');
}
/**
@@ -96,8 +99,8 @@ class DriverChainTest extends DoctrineTestCase
{
$companyDriver = $this->createMock(MappingDriver::class);
$defaultDriver = $this->createMock(MappingDriver::class);
$entityClassName = DriverChainEntity::class;
$managerClassName = 'Doctrine\Tests\Models\Company\CompanyManager';
$entityClassName = Model::class;
$managerClassName = Manager::class;
$chain = new MappingDriverChain();
$companyDriver->expects(self::never())
@@ -117,7 +120,7 @@ class DriverChainTest extends DoctrineTestCase
self::assertNull($chain->getDefaultDriver());
$chain->setDefaultDriver($defaultDriver);
$chain->addDriver($companyDriver, 'Doctrine\Tests\Models\Company');
$chain->addDriver($companyDriver, 'Doctrine\Tests\Persistence\Mapping\Fixtures\Manager');
$driver = $chain->getDefaultDriver();

View File

@@ -8,7 +8,12 @@ use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\Driver\FileDriver;
use Doctrine\Persistence\Mapping\Driver\FileLocator;
use Doctrine\Tests\DoctrineTestCase;
use Doctrine\Tests\Persistence\Mapping\Fixtures\AnotherGlobalClass;
use Doctrine\Tests\Persistence\Mapping\Fixtures\GlobalClass;
use Doctrine\Tests\Persistence\Mapping\Fixtures\NotLoadedClass;
use Doctrine\Tests\Persistence\Mapping\Fixtures\TestClassMetadata;
use PHPUnit\Framework\MockObject\MockObject;
use stdClass;
use function strpos;
@@ -31,9 +36,9 @@ class FileDriverTest extends DoctrineTestCase
$driver->setGlobalBasename('global');
$element = $driver->getElement('stdGlobal');
$element = $driver->getElement(GlobalClass::class);
self::assertSame($driver->stdGlobal, $element);
self::assertSame(GlobalClass::class, $element->getName());
}
public function testGetElementFromFile(): void
@@ -41,12 +46,12 @@ class FileDriverTest extends DoctrineTestCase
$locator = $this->newLocator();
$locator->expects(self::once())
->method('findMappingFile')
->with(self::equalTo('stdClass'))
->with(self::equalTo(stdClass::class))
->will(self::returnValue(__DIR__ . '/_files/stdClass.yml'));
$driver = $this->createTestFileDriver($locator);
self::assertSame($driver->stdClass, $driver->getElement('stdClass'));
self::assertSame(stdClass::class, $driver->getElement(stdClass::class)->getName());
}
public function testGetElementUpdatesClassCache(): void
@@ -56,29 +61,32 @@ class FileDriverTest extends DoctrineTestCase
// findMappingFile should only be called once
$locator->expects(self::once())
->method('findMappingFile')
->with(self::equalTo('stdClass'))
->with(self::equalTo(stdClass::class))
->will(self::returnValue(__DIR__ . '/_files/stdClass.yml'));
$driver = $this->createTestFileDriver($locator);
// not cached
self::assertSame($driver->stdClass, $driver->getElement('stdClass'));
self::assertSame(stdClass::class, $driver->getElement(stdClass::class)->getName());
// cached call
self::assertSame($driver->stdClass, $driver->getElement('stdClass'));
self::assertSame(stdClass::class, $driver->getElement(stdClass::class)->getName());
}
public function testGetAllClassNamesGlobalBasename(): void
{
$locator = $this->newLocator();
$locator->expects(self::any())->method('getAllClassNames')->with('global')->will(self::returnValue(['stdGlobal', 'stdGlobal2']));
$locator->expects(self::any())->method('getAllClassNames')->with('global')->will(self::returnValue([
GlobalClass::class,
AnotherGlobalClass::class,
]));
$driver = $this->createTestFileDriver($locator);
$driver->setGlobalBasename('global');
$classNames = $driver->getAllClassNames();
self::assertSame(['stdGlobal', 'stdGlobal2'], $classNames);
self::assertSame([GlobalClass::class, AnotherGlobalClass::class], $classNames);
}
public function testGetAllClassNamesFromMappingFile(): void
@@ -86,14 +94,13 @@ class FileDriverTest extends DoctrineTestCase
$locator = $this->newLocator();
$locator->expects(self::any())
->method('getAllClassNames')
->with(self::equalTo(''))
->will(self::returnValue(['stdClass']));
$driver = $this->createTestFileDriver($locator);
->with(self::equalTo(null))
->will(self::returnValue([stdClass::class]));
$driver = new TestFileDriver($locator);
$classNames = $driver->getAllClassNames();
self::assertSame(['stdClass'], $classNames);
self::assertSame([stdClass::class], $classNames);
}
public function testGetAllClassNamesBothSources(): void
@@ -102,14 +109,13 @@ class FileDriverTest extends DoctrineTestCase
$locator->expects(self::any())
->method('getAllClassNames')
->with(self::equalTo('global'))
->will(self::returnValue(['stdClass']));
$driver = $this->createTestFileDriver($locator);
->will(self::returnValue([stdClass::class]));
$driver = new TestFileDriver($locator);
$driver->setGlobalBasename('global');
$classNames = $driver->getAllClassNames();
self::assertSame(['stdGlobal', 'stdGlobal2', 'stdClass'], $classNames);
self::assertSame([GlobalClass::class, AnotherGlobalClass::class, stdClass::class], $classNames);
}
public function testGetAllClassNamesBothSourcesNoDupes(): void
@@ -118,20 +124,18 @@ class FileDriverTest extends DoctrineTestCase
$locator->expects(self::once())
->method('getAllClassNames')
->with(self::equalTo('global'))
->willReturn(['stdClass']);
$driver = $this->createTestFileDriver($locator);
$driver->setGlobalBasename('global');
->willReturn([stdClass::class]);
$locator->expects(self::once())
->method('findMappingFile')
->with('stdClass')
->willReturn('');
->with(self::equalTo(stdClass::class))
->will(self::returnValue(__DIR__ . '/_files/stdClass.yml'));
$driver = new TestFileDriver($locator);
$driver->setGlobalBasename('global');
$driver->getElement('stdClass');
$driver->getElement(stdClass::class);
$classNames = $driver->getAllClassNames();
self::assertSame(['stdGlobal', 'stdGlobal2', 'stdClass'], $classNames);
self::assertSame([GlobalClass::class, AnotherGlobalClass::class, stdClass::class], $classNames);
}
public function testIsNotTransient(): void
@@ -139,15 +143,15 @@ class FileDriverTest extends DoctrineTestCase
$locator = $this->newLocator();
$locator->expects(self::once())
->method('fileExists')
->with(self::equalTo('stdClass'))
->with(self::equalTo(stdClass::class))
->will(self::returnValue(true));
$driver = $this->createTestFileDriver($locator);
$driver->setGlobalBasename('global');
self::assertFalse($driver->isTransient('stdClass'));
self::assertFalse($driver->isTransient('stdGlobal'));
self::assertFalse($driver->isTransient('stdGlobal2'));
self::assertFalse($driver->isTransient(stdClass::class));
self::assertFalse($driver->isTransient(GlobalClass::class));
self::assertFalse($driver->isTransient(AnotherGlobalClass::class));
}
public function testIsTransient(): void
@@ -155,19 +159,19 @@ class FileDriverTest extends DoctrineTestCase
$locator = $this->newLocator();
$locator->expects(self::once())
->method('fileExists')
->with(self::equalTo('stdClass2'))
->with(self::equalTo(NotLoadedClass::class))
->will(self::returnValue(false));
$driver = $this->createTestFileDriver($locator);
self::assertTrue($driver->isTransient('stdClass2'));
self::assertTrue($driver->isTransient(NotLoadedClass::class));
}
public function testNonLocatorFallback(): void
{
$driver = $this->createTestFileDriver(__DIR__ . '/_files', '.yml');
self::assertTrue($driver->isTransient('stdClass2'));
self::assertFalse($driver->isTransient('stdClass'));
$driver = new TestFileDriver(__DIR__ . '/_files', '.yml');
self::assertTrue($driver->isTransient(NotLoadedClass::class));
self::assertFalse($driver->isTransient(stdClass::class));
}
/**
@@ -199,13 +203,13 @@ class FileDriverTest extends DoctrineTestCase
class TestFileDriver extends FileDriver
{
/** @var ClassMetadata */
/** @var ClassMetadata<object> */
public $stdGlobal;
/** @var ClassMetadata */
/** @var ClassMetadata<object> */
public $stdGlobal2;
/** @var ClassMetadata */
/** @var ClassMetadata<object> */
public $stdClass;
/**
@@ -215,14 +219,17 @@ class TestFileDriver extends FileDriver
{
if (strpos($file, 'global.yml') !== false) {
return [
'stdGlobal' => $this->stdGlobal,
'stdGlobal2' => $this->stdGlobal2,
GlobalClass::class => new TestClassMetadata(GlobalClass::class),
AnotherGlobalClass::class => new TestClassMetadata(AnotherGlobalClass::class),
];
}
return ['stdClass' => $this->stdClass];
return [stdClass::class => new TestClassMetadata(stdClass::class)];
}
/**
* @param ClassMetadata<object> $metadata
*/
public function loadMetadataForClass(string $className, ClassMetadata $metadata): void
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Persistence\Mapping\Fixtures;
final class AnotherGlobalClass
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Persistence\Mapping\Fixtures;
final class GlobalClass
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Persistence\Mapping\Fixtures\Manager;
final class Manager
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Persistence\Mapping\Fixtures;
final class Model
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Persistence\Mapping\Fixtures;
final class NotLoadedClass
{
}

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Persistence\Mapping\Fixtures;
use Doctrine\Persistence\Mapping\ClassMetadata;
use LogicException;
use ReflectionClass;
/**
* @template T of object
* @template-implements ClassMetadata<T>
*/
final class TestClassMetadata implements ClassMetadata
{
/**
* @var string
* @psalm-var class-string<T>
*/
private $className;
/**
* @psalm-param class-string<T> $className
*/
public function __construct(string $className)
{
$this->className = $className;
}
public function getName(): string
{
return $this->className;
}
/**
* {@inheritDoc}
*/
public function getIdentifier(): array
{
return ['id'];
}
public function getReflectionClass(): ReflectionClass
{
return new ReflectionClass($this->getName());
}
/**
* {@inheritDoc}
*/
public function isIdentifier(string $fieldName): bool
{
return false;
}
/**
* {@inheritDoc}
*/
public function hasField(string $fieldName): bool
{
return false;
}
/**
* {@inheritDoc}
*/
public function hasAssociation(string $fieldName)
{
return false;
}
/**
* {@inheritDoc}
*/
public function isSingleValuedAssociation(string $fieldName)
{
return false;
}
/**
* {@inheritDoc}
*/
public function isCollectionValuedAssociation(string $fieldName)
{
return false;
}
/**
* {@inheritDoc}
*/
public function getFieldNames(): array
{
return [];
}
/**
* {@inheritDoc}
*/
public function getIdentifierFieldNames(): array
{
return [];
}
/**
* {@inheritDoc}
*/
public function getAssociationNames(): array
{
return [];
}
/**
* {@inheritDoc}
*/
public function getTypeOfField(string $fieldName)
{
throw new LogicException('Not implemented');
}
/**
* {@inheritDoc}
*/
public function getAssociationTargetClass(string $assocName)
{
throw new LogicException('Not implemented');
}
public function isAssociationInverseSide(string $assocName): bool
{
return false;
}
/**
* {@inheritDoc}
*/
public function getAssociationMappedByTargetField(string $assocName)
{
throw new LogicException('Not implemented');
}
/**
* {@inheritDoc}
*/
public function getIdentifierValues(object $object): array
{
return [];
}
}

View File

@@ -30,6 +30,9 @@ class StaticPHPDriverTest extends DoctrineTestCase
class TestEntity
{
/**
* @psalm-param ClassMetadata<object> $metadata
*/
public static function loadMetadata(ClassMetadata $metadata): void
{
$metadata->getFieldNames();

View File

@@ -9,17 +9,27 @@ use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Mapping\ReflectionService;
/**
* @template CMTemplate of ClassMetadata
* @template-extends AbstractClassMetadataFactory<CMTemplate>
*/
class TestClassMetadataFactory extends AbstractClassMetadataFactory
{
/** @var MappingDriver */
public $driver;
/** @var ClassMetadata */
/**
* @var ClassMetadata
* @psalm-var CMTemplate
*/
public $metadata;
/** @var callable|null */
public $fallbackCallback;
/**
* @psalm-param CMTemplate $metadata
*/
public function __construct(MappingDriver $driver, ClassMetadata $metadata)
{
$this->driver = $driver;
@@ -39,6 +49,7 @@ class TestClassMetadataFactory extends AbstractClassMetadataFactory
protected function getFqcnFromAlias(string $namespaceAlias, string $simpleClassName): string
{
/** @psalm-var class-string */
return __NAMESPACE__ . '\\' . $simpleClassName;
}
@@ -84,4 +95,9 @@ class TestClassMetadataFactory extends AbstractClassMetadataFactory
return $class !== $name;
}
public function getCacheKey(string $realClassName): string
{
return parent::getCacheKey($realClassName);
}
}

View File

@@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase;
class ObjectManagerDecoratorTest extends TestCase
{
/** @var ObjectManager|MockObject */
/** @var MockObject&ObjectManager */
private $wrapped;
/** @var NullObjectManagerDecorator */
@@ -182,7 +182,7 @@ class ObjectManagerDecoratorTest extends TestCase
class NullObjectManagerDecorator extends ObjectManagerDecorator
{
/**
* @param ObjectManager|MockObject $wrapped
* @psalm-param ObjectManager&MockObject $wrapped
*/
public function __construct(ObjectManager $wrapped)
{

View File

@@ -143,7 +143,6 @@ class RuntimePublicReflectionPropertyTestProxyMock implements Proxy
*
* @return mixed[] Keys are the property names, and values are the default
* values for those properties.
*
* @phpstan-return array<string, mixed>
*/
public function __getLazyProperties()