Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

19
vendor/doctrine/persistence/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2006-2015 Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

17
vendor/doctrine/persistence/README.md vendored Normal file
View File

@ -0,0 +1,17 @@
# Doctrine Persistence
[![GitHub Actions][GA 4.0 image]][GA 4.0]
[![Code Coverage][Coverage 4.0 image]][CodeCov 4.0]
The Doctrine Persistence project is a library that provides common abstractions for object mapper persistence.
## More resources:
* [Website](https://www.doctrine-project.org/)
* [Documentation](https://www.doctrine-project.org/projects/doctrine-persistence/en/latest/index.html)
* [Downloads](https://github.com/doctrine/persistence/releases)
[Coverage 4.0 image]: https://codecov.io/gh/doctrine/persistence/branch/4.0.x/graph/badge.svg
[CodeCov 4.0]: https://codecov.io/gh/doctrine/persistence/branch/4.0.x
[GA 4.0 image]: https://github.com/doctrine/persistence/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0.x
[GA 4.0]: https://github.com/doctrine/persistence/actions/workflows/continuous-integration.yml?branch=4.0.x

195
vendor/doctrine/persistence/UPGRADE.md vendored Normal file
View File

@ -0,0 +1,195 @@
Note about upgrading: Doctrine uses static and runtime mechanisms to raise
awareness about deprecated code.
- Use of `@deprecated` docblock that is detected by IDEs (like PHPStorm) or
Static Analysis tools (like Psalm, phpstan)
- Use of our low-overhead runtime deprecation API, details:
https://github.com/doctrine/deprecations/
# Upgrade to 4.0
## BC Break: Removed `StaticReflectionService`
The class `Doctrine\Persistence\Mapping\StaticReflectionService` is removed
without replacement.
## BC Break: Narrowed `ReflectionService::getClass()` return type
The return type of `ReflectionService::getClass()` has been narrowed so that
`null` is no longer a valid return value.
## BC Break: Added `ObjectManager::isUninitializedObject()`
Classes implementing `Doctrine\Persistence\ObjectManager` must implement this
new method.
## BC Break: Added type declarations
The code base is now fully typed, meaning properties, parameters and return
type declarations have been added to all types.
## BC Break: Dropped support for Common proxies
Proxy objects implementing the `Doctrine\Common\Proxy\Proxy` interface are not
supported anymore. Implement `Doctrine\Persistence\Proxy` instead.
## BC Break: Removed deprecated ReflectionProperty overrides
Deprecated classes have been removed:
- `Doctrine\Persistence\Reflection\RuntimePublicReflectionProperty`
- `Doctrine\Persistence\Reflection\TypedNoDefaultRuntimePublicReflectionProperty`
# Upgrade to 3.4
## Deprecated `StaticReflectionService`
The class `Doctrine\Persistence\Mapping\StaticReflectionService` is deprecated
without replacement.
# Upgrade to 3.3
## Added method `ObjectManager::isUninitializedObject()`
Classes implementing `Doctrine\Persistence\ObjectManager` should implement the new
method. This method will be added to the interface in 4.0.
# Upgrade to 3.1
## Deprecated `RuntimePublicReflectionProperty`
Use `RuntimeReflectionProperty` instead.
# Upgrade to 3.0
## Removed `OnClearEventArgs::clearsAllEntities()` and `OnClearEventArgs::getEntityClass()`
These methods only make sense when partially clearing the object manager, which
is no longer possible.
The second argument of the constructor of `OnClearEventArgs` is removed as well.
## BC Break: removed `ObjectManagerAware`
Implement active record style functionality directly in your application, by
using a `postLoad` event.
## BC Break: removed `AnnotationDriver`
Use `ColocatedMappingDriver` instead.
## BC Break: Removed `MappingException::pathRequired()`
Use `MappingException::pathRequiredForDriver()` instead.
## BC Break: removed `LifecycleEventArgs::getEntity()`
Use `LifecycleEventArgs::getObject()` instead.
## BC Break: removed support for short namespace aliases
- `AbstractClassMetadataFactory::getFqcnFromAlias()` is removed.
- `ClassMetadataFactory` methods now require their `$className` argument to be an
actual FQCN.
## BC Break: removed `ObjectManager::merge()`
`ObjectManagerDecorator::merge()` is removed without replacement.
## BC Break: removed support for `doctrine/cache`
Removed support for using doctrine/cache for metadata caching. The
`setCacheDriver` and `getCacheDriver` methods have been removed from
`Doctrine\Persistence\Mapping\AbstractMetadata`. Please use `getCache` and
`setCache` with a PSR-6 implementation instead.
## BC Break: changed signatures
`$objectName` has been dropped from the signature of `ObjectManager::clear()`.
```diff
- public function clear($objectName = null)
+ public function clear(): void
```
Also, native parameter type declarations have been added on all public APIs.
Native return type declarations have not been added so that it is possible to
implement types compatible with both 2.x and 3.x.
## BC Break: Removed `PersistentObject`
Please implement this functionality directly in your application if you want
ActiveRecord style functionality.
# Upgrade to 2.5
## Deprecated `OnClearEventArgs::clearsAllEntities()` and `OnClearEventArgs::getEntityClass()`
These methods only make sense when partially clearing the object manager, which
is deprecated.
Passing a second argument to the constructor of `OnClearEventArgs` is
deprecated as well.
## Deprecated `ObjectManagerAware`
Along with deprecating `PersistentObject`, deprecating `ObjectManagerAware`
means deprecating support for active record, which already came with a word of
warning. Please implement this directly in your application with a `postLoad`
event if you need active record style functionality.
## Deprecated `MappingException::pathRequired()`
`MappingException::pathRequiredForDriver()` should be used instead.
# Upgrade to 2.4
## Deprecated `AnnotationDriver`
Since attributes were introduced in PHP 8.0, annotations are deprecated.
`AnnotationDriver` is an abstract class that is used when implementing concrete
annotation drivers in dependent packages. It is deprecated in favor of using
`ColocatedMappingDriver` to implement both annotation and attribute based
drivers. This will involve implementing `isTransient()` as well as
`__construct()` and `getReader()` to retain backward compatibility.
# Upgrade to 2.3
## Deprecated using short namespace alias syntax in favor of `::class` syntax.
Before:
```php
$objectManager->find('MyPackage:MyClass', $id);
$objectManager->createQuery('SELECT u FROM MyPackage:MyClass');
```
After:
```php
$objectManager->find(MyClass::class, $id);
$objectManager->createQuery('SELECT u FROM '. MyClass::class);
```
# Upgrade to 2.2
## Deprecated `doctrine/cache` usage 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.
# Upgrade to 1.2
## Deprecated `ObjectManager::merge()` and `ObjectManager::detach()`
Please handle merge operations in your application, and use
`ObjectManager::clear()` instead.
## Deprecated `PersistentObject`
Please implement this functionality directly in your application if you want
ActiveRecord style functionality.

View File

@ -0,0 +1,52 @@
{
"name": "doctrine/persistence",
"type": "library",
"description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.",
"keywords": [
"persistence",
"object",
"mapper",
"orm",
"odm"
],
"homepage": "https://www.doctrine-project.org/projects/persistence.html",
"license": "MIT",
"authors": [
{"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
{"name": "Roman Borschel", "email": "roman@code-factory.org"},
{"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
{"name": "Jonathan Wage", "email": "jonwage@gmail.com"},
{"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"},
{"name": "Marco Pivetta", "email": "ocramius@gmail.com"}
],
"require": {
"php": "^8.1",
"doctrine/event-manager": "^1 || ^2",
"psr/cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"phpstan/phpstan": "1.12.7",
"phpstan/phpstan-phpunit": "^1",
"phpstan/phpstan-strict-rules": "^1.6",
"doctrine/coding-standard": "^12",
"phpunit/phpunit": "^9.6",
"symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0",
"symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0"
},
"autoload": {
"psr-4": {
"Doctrine\\Persistence\\": "src/Persistence"
}
},
"autoload-dev": {
"psr-4": {
"Doctrine\\Tests\\": "tests"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"composer/package-versions-deprecated": true
}
}
}

View File

@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence;
use InvalidArgumentException;
use ReflectionClass;
use function assert;
use function sprintf;
/**
* Abstract implementation of the ManagerRegistry contract.
*/
abstract class AbstractManagerRegistry implements ManagerRegistry
{
/**
* @param array<string, string> $connections
* @param array<string, string> $managers
* @phpstan-param class-string $proxyInterfaceName
*/
public function __construct(
private readonly string $name,
private array $connections,
private array $managers,
private readonly string $defaultConnection,
private readonly string $defaultManager,
private readonly string $proxyInterfaceName,
) {
}
/**
* Fetches/creates the given services.
*
* A service in this context is connection or a manager instance.
*
* @param string $name The name of the service.
*
* @return object The instance of the given service.
*/
abstract protected function getService(string $name): object;
/**
* Resets the given services.
*
* A service in this context is connection or a manager instance.
*
* @param string $name The name of the service.
*/
abstract protected function resetService(string $name): void;
/** Gets the name of the registry. */
public function getName(): string
{
return $this->name;
}
public function getConnection(string|null $name = null): object
{
if ($name === null) {
$name = $this->defaultConnection;
}
if (! isset($this->connections[$name])) {
throw new InvalidArgumentException(
sprintf('Doctrine %s Connection named "%s" does not exist.', $this->name, $name),
);
}
return $this->getService($this->connections[$name]);
}
/**
* {@inheritDoc}
*/
public function getConnectionNames(): array
{
return $this->connections;
}
/**
* {@inheritDoc}
*/
public function getConnections(): array
{
$connections = [];
foreach ($this->connections as $name => $id) {
$connections[$name] = $this->getService($id);
}
return $connections;
}
public function getDefaultConnectionName(): string
{
return $this->defaultConnection;
}
public function getDefaultManagerName(): string
{
return $this->defaultManager;
}
/**
* {@inheritDoc}
*
* @throws InvalidArgumentException
*/
public function getManager(string|null $name = null): ObjectManager
{
if ($name === null) {
$name = $this->defaultManager;
}
if (! isset($this->managers[$name])) {
throw new InvalidArgumentException(
sprintf('Doctrine %s Manager named "%s" does not exist.', $this->name, $name),
);
}
$service = $this->getService($this->managers[$name]);
assert($service instanceof ObjectManager);
return $service;
}
public function getManagerForClass(string $class): ObjectManager|null
{
$proxyClass = new ReflectionClass($class);
if ($proxyClass->isAnonymous()) {
return null;
}
if ($proxyClass->implementsInterface($this->proxyInterfaceName)) {
$parentClass = $proxyClass->getParentClass();
if ($parentClass === false) {
return null;
}
$class = $parentClass->getName();
}
foreach ($this->managers as $id) {
$manager = $this->getService($id);
assert($manager instanceof ObjectManager);
if (! $manager->getMetadataFactory()->isTransient($class)) {
return $manager;
}
}
return null;
}
/**
* {@inheritDoc}
*/
public function getManagerNames(): array
{
return $this->managers;
}
/**
* {@inheritDoc}
*/
public function getManagers(): array
{
$managers = [];
foreach ($this->managers as $name => $id) {
$manager = $this->getService($id);
assert($manager instanceof ObjectManager);
$managers[$name] = $manager;
}
return $managers;
}
public function getRepository(
string $persistentObject,
string|null $persistentManagerName = null,
): ObjectRepository {
return $this
->selectManager($persistentObject, $persistentManagerName)
->getRepository($persistentObject);
}
public function resetManager(string|null $name = null): ObjectManager
{
if ($name === null) {
$name = $this->defaultManager;
}
if (! isset($this->managers[$name])) {
throw new InvalidArgumentException(sprintf('Doctrine %s Manager named "%s" does not exist.', $this->name, $name));
}
// force the creation of a new document manager
// if the current one is closed
$this->resetService($this->managers[$name]);
return $this->getManager($name);
}
/** @phpstan-param class-string $persistentObject */
private function selectManager(
string $persistentObject,
string|null $persistentManagerName = null,
): ObjectManager {
if ($persistentManagerName !== null) {
return $this->getManager($persistentManagerName);
}
return $this->getManagerForClass($persistentObject) ?? $this->getManager();
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence;
/**
* Contract covering connection for a Doctrine persistence layer ManagerRegistry class to implement.
*/
interface ConnectionRegistry
{
/**
* Gets the default connection name.
*
* @return string The default connection name.
*/
public function getDefaultConnectionName(): string;
/**
* Gets the named connection.
*
* @param string|null $name The connection name (null for the default one).
*/
public function getConnection(string|null $name = null): object;
/**
* Gets an array of all registered connections.
*
* @return array<string, object> An array of Connection instances.
*/
public function getConnections(): array;
/**
* Gets all connection names.
*
* @return array<string, string> An array of connection names.
*/
public function getConnectionNames(): array;
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Event;
use Doctrine\Common\EventArgs;
use Doctrine\Persistence\ObjectManager;
/**
* Lifecycle Events are triggered by the UnitOfWork during lifecycle transitions
* of entities.
*
* @template-covariant TObjectManager of ObjectManager
*/
class LifecycleEventArgs extends EventArgs
{
/** @phpstan-param TObjectManager $objectManager */
public function __construct(
private readonly object $object,
private readonly ObjectManager $objectManager,
) {
}
/** Retrieves the associated object. */
public function getObject(): object
{
return $this->object;
}
/**
* Retrieves the associated ObjectManager.
*
* @phpstan-return TObjectManager
*/
public function getObjectManager(): ObjectManager
{
return $this->objectManager;
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Event;
use Doctrine\Common\EventArgs;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\ObjectManager;
/**
* Class that holds event arguments for a loadMetadata event.
*
* @template-covariant TClassMetadata of ClassMetadata<object>
* @template-covariant TObjectManager of ObjectManager
*/
class LoadClassMetadataEventArgs extends EventArgs
{
/**
* @phpstan-param TClassMetadata $classMetadata
* @phpstan-param TObjectManager $objectManager
*/
public function __construct(
private readonly ClassMetadata $classMetadata,
private readonly ObjectManager $objectManager,
) {
}
/**
* Retrieves the associated ClassMetadata.
*
* @phpstan-return TClassMetadata
*/
public function getClassMetadata(): ClassMetadata
{
return $this->classMetadata;
}
/**
* Retrieves the associated ObjectManager.
*
* @phpstan-return TObjectManager
*/
public function getObjectManager(): ObjectManager
{
return $this->objectManager;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Event;
use Doctrine\Common\EventArgs;
use Doctrine\Persistence\ObjectManager;
/**
* Provides event arguments for the preFlush event.
*
* @template-covariant TObjectManager of ObjectManager
*/
class ManagerEventArgs extends EventArgs
{
/** @phpstan-param TObjectManager $objectManager */
public function __construct(
private readonly ObjectManager $objectManager,
) {
}
/**
* Retrieves the associated ObjectManager.
*
* @phpstan-return TObjectManager
*/
public function getObjectManager(): ObjectManager
{
return $this->objectManager;
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Event;
use Doctrine\Common\EventArgs;
use Doctrine\Persistence\ObjectManager;
/**
* Provides event arguments for the onClear event.
*
* @template-covariant TObjectManager of ObjectManager
*/
class OnClearEventArgs extends EventArgs
{
/**
* @param ObjectManager $objectManager The object manager.
* @phpstan-param TObjectManager $objectManager
*/
public function __construct(private readonly ObjectManager $objectManager)
{
}
/**
* Retrieves the associated ObjectManager.
*
* @phpstan-return TObjectManager
*/
public function getObjectManager(): ObjectManager
{
return $this->objectManager;
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Event;
use Doctrine\Persistence\ObjectManager;
use InvalidArgumentException;
use function sprintf;
/**
* Class that holds event arguments for a preUpdate event.
*
* @template-covariant TObjectManager of ObjectManager
* @extends LifecycleEventArgs<TObjectManager>
*/
class PreUpdateEventArgs extends LifecycleEventArgs
{
/** @var array<string, array<int, mixed>> */
private array $entityChangeSet;
/**
* @param array<string, array<int, mixed>> $changeSet
* @phpstan-param TObjectManager $objectManager
*/
public function __construct(object $entity, ObjectManager $objectManager, array &$changeSet)
{
parent::__construct($entity, $objectManager);
$this->entityChangeSet = &$changeSet;
}
/**
* Retrieves the entity changeset.
*
* @return array<string, array<int, mixed>>
*/
public function getEntityChangeSet(): array
{
return $this->entityChangeSet;
}
/** Checks if field has a changeset. */
public function hasChangedField(string $field): bool
{
return isset($this->entityChangeSet[$field]);
}
/** Gets the old value of the changeset of the changed field. */
public function getOldValue(string $field): mixed
{
$this->assertValidField($field);
return $this->entityChangeSet[$field][0];
}
/** Gets the new value of the changeset of the changed field. */
public function getNewValue(string $field): mixed
{
$this->assertValidField($field);
return $this->entityChangeSet[$field][1];
}
/** Sets the new value of this field. */
public function setNewValue(string $field, mixed $value): void
{
$this->assertValidField($field);
$this->entityChangeSet[$field][1] = $value;
}
/**
* Asserts the field exists in changeset.
*
* @throws InvalidArgumentException
*/
private function assertValidField(string $field): void
{
if (! isset($this->entityChangeSet[$field])) {
throw new InvalidArgumentException(sprintf(
'Field "%s" is not a valid field of the entity "%s" in PreUpdateEventArgs.',
$field,
$this->getObject()::class,
));
}
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence;
/**
* Contract covering object managers for a Doctrine persistence layer ManagerRegistry class to implement.
*/
interface ManagerRegistry extends ConnectionRegistry
{
/**
* Gets the default object manager name.
*
* @return string The default object manager name.
*/
public function getDefaultManagerName(): string;
/**
* Gets a named object manager.
*
* @param string|null $name The object manager name (null for the default one).
*/
public function getManager(string|null $name = null): ObjectManager;
/**
* Gets an array of all registered object managers.
*
* @return array<string, ObjectManager> An array of ObjectManager instances
*/
public function getManagers(): array;
/**
* Resets a named object manager.
*
* This method is useful when an object manager has been closed
* because of a rollbacked transaction AND when you think that
* it makes sense to get a new one to replace the closed one.
*
* Be warned that you will get a brand new object manager as
* the existing one is not useable anymore. This means that any
* other object with a dependency on this object manager will
* hold an obsolete reference. You can inject the registry instead
* to avoid this problem.
*
* @param string|null $name The object manager name (null for the default one).
*/
public function resetManager(string|null $name = null): ObjectManager;
/**
* Gets all object manager names and associated service IDs. A service ID
* is a string that allows to obtain an object manager, typically from a
* PSR-11 container.
*
* @return array<string,string> An array with object manager names as keys,
* and service IDs as values.
*/
public function getManagerNames(): array;
/**
* Gets the ObjectRepository for a persistent object.
*
* @param string $persistentObject The name of the persistent object.
* @param string|null $persistentManagerName The object manager name (null for the default one).
* @phpstan-param class-string<T> $persistentObject
*
* @phpstan-return ObjectRepository<T>
*
* @template T of object
*/
public function getRepository(
string $persistentObject,
string|null $persistentManagerName = null,
): ObjectRepository;
/**
* Gets the object manager associated with a given class.
*
* @param class-string $class A persistent object class name.
*/
public function getManagerForClass(string $class): ObjectManager|null;
}

View File

@ -0,0 +1,460 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Proxy;
use Psr\Cache\CacheItemPoolInterface;
use ReflectionClass;
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 class_exists;
use function ltrim;
use function str_contains;
use function str_replace;
use function strrpos;
use function substr;
/**
* The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
* metadata mapping informations of a class which describes how a class should be mapped
* 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
{
/** Salt used by specific Object Manager implementation. */
protected string $cacheSalt = '__CLASSMETADATA__';
private CacheItemPoolInterface|null $cache = null;
/**
* @var array<string, ClassMetadata>
* @phpstan-var CMTemplate[]
*/
private array $loadedMetadata = [];
protected bool $initialized = false;
private ReflectionService|null $reflectionService = null;
private ProxyClassNameResolver|null $proxyClassNameResolver = null;
public function setCache(CacheItemPoolInterface $cache): void
{
$this->cache = $cache;
}
final protected function getCache(): CacheItemPoolInterface|null
{
return $this->cache;
}
/**
* Returns an array of all the loaded metadata currently in memory.
*
* @return ClassMetadata[]
* @phpstan-return CMTemplate[]
*/
public function getLoadedMetadata(): array
{
return $this->loadedMetadata;
}
/**
* {@inheritDoc}
*/
public function getAllMetadata(): array
{
if (! $this->initialized) {
$this->initialize();
}
$driver = $this->getDriver();
$metadata = [];
foreach ($driver->getAllClassNames() as $className) {
$metadata[] = $this->getMetadataFor($className);
}
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.
*/
abstract protected function initialize(): void;
/** Returns the mapping driver implementation. */
abstract protected function getDriver(): MappingDriver;
/**
* Wakes up reflection after ClassMetadata gets unserialized from cache.
*
* @phpstan-param CMTemplate $class
*/
abstract protected function wakeupReflection(
ClassMetadata $class,
ReflectionService $reflService,
): void;
/**
* Initializes Reflection after ClassMetadata was constructed.
*
* @phpstan-param CMTemplate $class
*/
abstract protected function initializeReflection(
ClassMetadata $class,
ReflectionService $reflService,
): void;
/**
* Checks whether the class metadata is an entity.
*
* This method should return false for mapped superclasses or embedded classes.
*
* @phpstan-param CMTemplate $class
*/
abstract protected function isEntity(ClassMetadata $class): bool;
/**
* Removes the prepended backslash of a class string to conform with how php outputs class names
*
* @phpstan-param class-string $className
*
* @phpstan-return class-string
*/
private function normalizeClassName(string $className): string
{
return ltrim($className, '\\');
}
/**
* {@inheritDoc}
*
* @throws ReflectionException
* @throws MappingException
*/
public function getMetadataFor(string $className): ClassMetadata
{
$className = $this->normalizeClassName($className);
if (isset($this->loadedMetadata[$className])) {
return $this->loadedMetadata[$className];
}
if (class_exists($className, false) && (new ReflectionClass($className))->isAnonymous()) {
throw MappingException::classIsAnonymous($className);
}
if (! class_exists($className, false) && str_contains($className, ':')) {
throw MappingException::nonExistingClass($className);
}
$realClassName = $this->getRealClass($className);
if (isset($this->loadedMetadata[$realClassName])) {
// We do not have the alias name in the map, include it
return $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
}
try {
if ($this->cache !== null) {
$cached = $this->cache->getItem($this->getCacheKey($realClassName))->get();
if ($cached instanceof ClassMetadata) {
/** @phpstan-var CMTemplate $cached */
$this->loadedMetadata[$realClassName] = $cached;
$this->wakeupReflection($cached, $this->getReflectionService());
} else {
$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);
}
} catch (MappingException $loadingException) {
$fallbackMetadataResponse = $this->onNotFoundMetadata($realClassName);
if ($fallbackMetadataResponse === null) {
throw $loadingException;
}
$this->loadedMetadata[$realClassName] = $fallbackMetadataResponse;
}
if ($className !== $realClassName) {
// We do not have the alias name in the map, include it
$this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName];
}
return $this->loadedMetadata[$className];
}
public function hasMetadataFor(string $className): bool
{
$className = $this->normalizeClassName($className);
return isset($this->loadedMetadata[$className]);
}
/**
* Sets the metadata descriptor for a specific class.
*
* NOTE: This is only useful in very special cases, like when generating proxy classes.
*
* @phpstan-param class-string $className
* @phpstan-param CMTemplate $class
*/
public function setMetadataFor(string $className, ClassMetadata $class): void
{
$this->loadedMetadata[$this->normalizeClassName($className)] = $class;
}
/**
* Gets an array of parent classes for the given entity class.
*
* @phpstan-param class-string $name
*
* @return string[]
* @phpstan-return list<class-string>
*/
protected function getParentClasses(string $name): array
{
// Collect parent classes, ignoring transient (not-mapped) classes.
$parentClasses = [];
foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) {
if ($this->getDriver()->isTransient($parentClass)) {
continue;
}
$parentClasses[] = $parentClass;
}
return $parentClasses;
}
/**
* Loads the metadata of the class in question and all it's ancestors whose metadata
* is still not loaded.
*
* Important: The class $name does not necessarily exist at this point here.
* Scenarios in a code-generation setup might have access to XML/YAML
* Mapping files without the actual PHP code existing here. That is why the
* {@see \Doctrine\Persistence\Mapping\ReflectionService} interface
* should be used for reflection.
*
* @param string $name The name of the class for which the metadata should get loaded.
* @phpstan-param class-string $name
*
* @return array<int, string>
* @phpstan-return list<string>
*/
protected function loadMetadata(string $name): array
{
if (! $this->initialized) {
$this->initialize();
}
$loaded = [];
$parentClasses = $this->getParentClasses($name);
$parentClasses[] = $name;
// Move down the hierarchy of parent classes, starting from the topmost class
$parent = null;
$rootEntityFound = false;
$visited = [];
$reflService = $this->getReflectionService();
foreach ($parentClasses as $className) {
if (isset($this->loadedMetadata[$className])) {
$parent = $this->loadedMetadata[$className];
if ($this->isEntity($parent)) {
$rootEntityFound = true;
array_unshift($visited, $className);
}
continue;
}
$class = $this->newClassMetadataInstance($className);
$this->initializeReflection($class, $reflService);
$this->doLoadMetadata($class, $parent, $rootEntityFound, $visited);
$this->loadedMetadata[$className] = $class;
$parent = $class;
if ($this->isEntity($class)) {
$rootEntityFound = true;
array_unshift($visited, $className);
}
$this->wakeupReflection($class, $reflService);
$loaded[] = $className;
}
return $loaded;
}
/**
* Provides a fallback hook for loading metadata when loading failed due to reflection/mapping exceptions
*
* Override this method to implement a fallback strategy for failed metadata loading
*
* @phpstan-return CMTemplate|null
*/
protected function onNotFoundMetadata(string $className): ClassMetadata|null
{
return null;
}
/**
* Actually loads the metadata from the underlying metadata.
*
* @param bool $rootEntityFound True when there is another entity (non-mapped superclass) class above the current class in the PHP class hierarchy.
* @param list<class-string> $nonSuperclassParents All parent class names that are not marked as mapped superclasses, with the direct parent class being the first and the root entity class the last element.
* @phpstan-param CMTemplate $class
* @phpstan-param CMTemplate|null $parent
*/
abstract protected function doLoadMetadata(
ClassMetadata $class,
ClassMetadata|null $parent,
bool $rootEntityFound,
array $nonSuperclassParents,
): void;
/**
* Creates a new ClassMetadata instance for the given class name.
*
* @phpstan-param class-string<T> $className
*
* @return ClassMetadata<T>
* @phpstan-return CMTemplate
*
* @template T of object
*/
abstract protected function newClassMetadataInstance(string $className): ClassMetadata;
public function isTransient(string $className): bool
{
if (! $this->initialized) {
$this->initialize();
}
if (class_exists($className, false) && (new ReflectionClass($className))->isAnonymous()) {
return false;
}
if (! class_exists($className, false) && str_contains($className, ':')) {
throw MappingException::nonExistingClass($className);
}
/** @phpstan-var class-string $className */
return $this->getDriver()->isTransient($className);
}
/** Sets the reflectionService. */
public function setReflectionService(ReflectionService $reflectionService): void
{
$this->reflectionService = $reflectionService;
}
/** Gets the reflection service associated with this metadata factory. */
public function getReflectionService(): ReflectionService
{
if ($this->reflectionService === null) {
$this->reflectionService = new RuntimeReflectionService();
}
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.
*
* @phpstan-param class-string<Proxy<T>>|class-string<T> $class
*
* @phpstan-return class-string<T>
*
* @template T of object
*/
private function getRealClass(string $class): string
{
if ($this->proxyClassNameResolver === null) {
$this->createDefaultProxyClassNameResolver();
}
assert($this->proxyClassNameResolver !== null);
return $this->proxyClassNameResolver->resolveClassName($class);
}
private function createDefaultProxyClassNameResolver(): void
{
$this->proxyClassNameResolver = new class implements ProxyClassNameResolver {
/**
* @phpstan-param class-string<Proxy<T>>|class-string<T> $className
*
* @phpstan-return class-string<T>
*
* @template T of object
*/
public function resolveClassName(string $className): string
{
$pos = strrpos($className, '\\' . Proxy::MARKER . '\\');
if ($pos === false) {
/** @phpstan-var class-string<T> */
return $className;
}
/** @phpstan-var class-string<T> */
return substr($className, $pos + Proxy::MARKER_LENGTH + 2);
}
};
}
}

View File

@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping;
use ReflectionClass;
/**
* Contract for a Doctrine persistence layer ClassMetadata class to implement.
*
* @template-covariant T of object
*/
interface ClassMetadata
{
/**
* Gets the fully-qualified class name of this persistent class.
*
* @phpstan-return class-string<T>
*/
public function getName(): string;
/**
* Gets the mapped identifier field name.
*
* The returned structure is an array of the identifier field names.
*
* @return array<int, string>
* @phpstan-return list<string>
*/
public function getIdentifier(): array;
/**
* Gets the ReflectionClass instance for this mapped class.
*
* @return ReflectionClass<T>
*/
public function getReflectionClass(): ReflectionClass;
/** Checks if the given field name is a mapped identifier for this class. */
public function isIdentifier(string $fieldName): bool;
/** Checks if the given field is a mapped property for this class. */
public function hasField(string $fieldName): bool;
/** Checks if the given field is a mapped association for this class. */
public function hasAssociation(string $fieldName): bool;
/** Checks if the given field is a mapped single valued association for this class. */
public function isSingleValuedAssociation(string $fieldName): bool;
/** Checks if the given field is a mapped collection valued association for this class. */
public function isCollectionValuedAssociation(string $fieldName): bool;
/**
* A numerically indexed list of field names of this persistent class.
*
* This array includes identifier fields if present on this class.
*
* @return array<int, string>
*/
public function getFieldNames(): array;
/**
* Returns an array of identifier field names numerically indexed.
*
* @return array<int, string>
*/
public function getIdentifierFieldNames(): array;
/**
* Returns a numerically indexed list of association names of this persistent class.
*
* This array includes identifier associations if present on this class.
*
* @return array<int, string>
*/
public function getAssociationNames(): array;
/**
* Returns a type name of this field.
*
* This type names can be implementation specific but should at least include the php types:
* integer, string, boolean, float/double, datetime.
*/
public function getTypeOfField(string $fieldName): string|null;
/**
* Returns the target class name of the given association.
*
* @phpstan-return class-string|null
*/
public function getAssociationTargetClass(string $assocName): string|null;
/** Checks if the association is the inverse side of a bidirectional association. */
public function isAssociationInverseSide(string $assocName): bool;
/** Returns the target field of the owning side of the association. */
public function getAssociationMappedByTargetField(string $assocName): string;
/**
* Returns the identifier of this object as an array with field name as key.
*
* Has to return an empty array if no identifier isset.
*
* @return array<string, mixed>
*/
public function getIdentifierValues(object $object): array;
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping;
/**
* Contract for a Doctrine persistence layer ClassMetadata class to implement.
*
* @template T of ClassMetadata
*/
interface ClassMetadataFactory
{
/**
* Forces the factory to load the metadata of all classes known to the underlying
* mapping driver.
*
* @return ClassMetadata[] The ClassMetadata instances of all mapped classes.
* @phpstan-return list<T>
*/
public function getAllMetadata(): array;
/**
* Gets the class metadata descriptor for a class.
*
* @param class-string $className The name of the class.
*
* @phpstan-return T
*/
public function getMetadataFor(string $className): ClassMetadata;
/**
* Checks whether the factory has the metadata for a class loaded already.
*
* @param class-string $className
*
* @return bool TRUE if the metadata of the class in question is already loaded, FALSE otherwise.
*/
public function hasMetadataFor(string $className): bool;
/**
* Sets the metadata descriptor for a specific class.
*
* @param class-string $className
* @phpstan-param T $class
*/
public function setMetadataFor(string $className, ClassMetadata $class): void;
/**
* 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.
*
* @phpstan-param class-string $className
*/
public function isTransient(string $className): bool;
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
/**
* ClassLocator is an interface for classes that can provide a list of class names.
*/
interface ClassLocator
{
/** @return list<class-string> */
public function getClassNames(): array;
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
/**
* Basic implementation of ClassLocator that passes a list of class names.
*/
final class ClassNames implements ClassLocator
{
/** @param list<class-string> $classNames */
public function __construct(
private array $classNames,
) {
}
/** @return list<class-string> */
public function getClassNames(): array
{
return $this->classNames;
}
}

View File

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
use Doctrine\Persistence\Mapping\MappingException;
use function array_filter;
use function array_merge;
use function array_unique;
use function array_values;
/**
* The ColocatedMappingDriver reads the mapping metadata located near the code.
*/
trait ColocatedMappingDriver
{
private ClassLocator|null $classLocator = null;
/**
* The directory paths where to look for mapping files.
*
* @var array<int, string>
*/
protected array $paths = [];
/**
* The paths excluded from path where to look for mapping files.
*
* @var array<int, string>
*/
protected array $excludePaths = [];
/** The file extension of mapping documents. */
protected string $fileExtension = '.php';
/**
* Cache for {@see getAllClassNames()}.
*
* @var array<int, string>|null
* @phpstan-var list<class-string>|null
*/
protected array|null $classNames = null;
/**
* Appends lookup paths to metadata driver.
*
* @param array<int, string> $paths
*/
public function addPaths(array $paths): void
{
$this->paths = array_unique(array_merge($this->paths, $paths));
}
/**
* Retrieves the defined metadata lookup paths.
*
* @return array<int, string>
*/
public function getPaths(): array
{
return $this->paths;
}
/**
* Append exclude lookup paths to a metadata driver.
*
* @param string[] $paths
*/
public function addExcludePaths(array $paths): void
{
$this->excludePaths = array_unique(array_merge($this->excludePaths, $paths));
}
/**
* Retrieve the defined metadata lookup exclude paths.
*
* @return array<int, string>
*/
public function getExcludePaths(): array
{
return $this->excludePaths;
}
/** Gets the file extension used to look for mapping files under. */
public function getFileExtension(): string
{
return $this->fileExtension;
}
/** Sets the file extension used to look for mapping files under. */
public function setFileExtension(string $fileExtension): void
{
$this->fileExtension = $fileExtension;
}
/**
* {@inheritDoc}
*
* Returns whether the class with the specified name is transient. Only non-transient
* classes, that is entities and mapped superclasses, should have their metadata loaded.
*
* @phpstan-param class-string $className
*/
abstract public function isTransient(string $className): bool;
/**
* Gets the names of all mapped classes known to this driver.
*
* @return string[] The names of all mapped classes known to this driver.
* @phpstan-return list<class-string>
*/
public function getAllClassNames(): array
{
if ($this->classNames !== null) {
return $this->classNames;
}
if ($this->paths === [] && $this->classLocator === null) {
throw MappingException::pathRequiredForDriver(static::class);
}
$classNames = $this->classLocator?->getClassNames() ?? [];
if ($this->paths !== []) {
$classNames = array_unique([
...FileClassLocator::createFromDirectories($this->paths, $this->excludePaths, $this->fileExtension)->getClassNames(),
...$classNames,
]);
}
return $this->classNames = array_values(array_filter(
$classNames,
fn (string $className): bool => ! $this->isTransient($className),
));
}
}

View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
use Doctrine\Persistence\Mapping\MappingException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use function array_unique;
use function assert;
use function is_dir;
use function is_file;
use function is_string;
use function str_replace;
use const DIRECTORY_SEPARATOR;
/**
* Locates the file that contains the metadata information for a given class name.
*
* This behavior is independent of the actual content of the file. It just detects
* the file which is responsible for the given class name.
*/
class DefaultFileLocator implements FileLocator
{
/**
* The paths where to look for mapping files.
*
* @var array<int, string>
*/
protected array $paths = [];
/** The file extension of mapping documents. */
protected string|null $fileExtension;
/**
* Initializes a new FileDriver that looks in the given path(s) for mapping
* documents and operates in the specified operating mode.
*
* @param string|array<int, string> $paths One or multiple paths where mapping documents
* can be found.
* @param string|null $fileExtension The file extension of mapping documents,
* usually prefixed with a dot.
*/
public function __construct(string|array $paths, string|null $fileExtension = null)
{
$this->addPaths((array) $paths);
$this->fileExtension = $fileExtension;
}
/**
* Appends lookup paths to metadata driver.
*
* @param array<int, string> $paths
*/
public function addPaths(array $paths): void
{
$this->paths = array_unique([...$this->paths, ...$paths]);
}
/**
* Retrieves the defined metadata lookup paths.
*
* @return array<int, string>
*/
public function getPaths(): array
{
return $this->paths;
}
/** Gets the file extension used to look for mapping files under. */
public function getFileExtension(): string|null
{
return $this->fileExtension;
}
/**
* Sets the file extension used to look for mapping files under.
*
* @param string|null $fileExtension The file extension to set.
*/
public function setFileExtension(string|null $fileExtension): void
{
$this->fileExtension = $fileExtension;
}
public function findMappingFile(string $className): string
{
$fileName = str_replace('\\', '.', $className) . $this->fileExtension;
// Check whether file exists
foreach ($this->paths as $path) {
if (is_file($path . DIRECTORY_SEPARATOR . $fileName)) {
return $path . DIRECTORY_SEPARATOR . $fileName;
}
}
throw MappingException::mappingFileNotFound($className, $fileName);
}
/**
* {@inheritDoc}
*/
public function getAllClassNames(string $globalBasename): array
{
if ($this->paths === []) {
return [];
}
$classes = [];
foreach ($this->paths as $path) {
if (! is_dir($path)) {
throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path);
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::LEAVES_ONLY,
);
foreach ($iterator as $file) {
$fileName = $file->getBasename($this->fileExtension);
if ($fileName === $file->getBasename() || $fileName === $globalBasename) {
continue;
}
// NOTE: All files found here means classes are not transient!
assert(is_string($fileName));
/** @phpstan-var class-string */
$class = str_replace('.', '\\', $fileName);
$classes[] = $class;
}
}
return $classes;
}
public function fileExists(string $className): bool
{
$fileName = str_replace('\\', '.', $className) . $this->fileExtension;
// Check whether file exists
foreach ($this->paths as $path) {
if (is_file($path . DIRECTORY_SEPARATOR . $fileName)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
use AppendIterator;
use CallbackFilterIterator;
use Doctrine\Persistence\Mapping\MappingException;
use FilesystemIterator;
use InvalidArgumentException;
use Iterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use RegexIterator;
use SplFileInfo;
use function array_key_exists;
use function array_map;
use function assert;
use function get_debug_type;
use function get_declared_classes;
use function is_dir;
use function preg_quote;
use function realpath;
use function sprintf;
use function str_replace;
use function str_starts_with;
/**
* ClassLocator implementation that uses a list of file names to locate PHP files
* and extract class names from them.
*
* It is compatible with the Symfony Finder component, but does not require it.
*/
final class FileClassLocator implements ClassLocator
{
/** @param iterable<SplFileInfo> $files An iterable of files to include. */
public function __construct(
private iterable $files,
) {
}
/** @return list<class-string> */
public function getClassNames(): array
{
$includedFiles = [];
foreach ($this->files as $file) {
// @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue
assert($file instanceof SplFileInfo, new InvalidArgumentException(sprintf('Expected an iterable of SplFileInfo, got %s', get_debug_type($file))));
// Skip non-files
if (! $file->isFile()) {
continue;
}
// getRealPath() returns false if the file is in a phar archive
// @phpstan-ignore ternary.shortNotAllowed (false is the only falsy value getRealPath() may return)
$fileName = $file->getRealPath() ?: $file->getPathname();
$includedFiles[$fileName] = true;
require_once $fileName;
}
$classes = [];
foreach (get_declared_classes() as $className) {
$fileName = (new ReflectionClass($className))->getFileName();
if ($fileName === false || ! array_key_exists($fileName, $includedFiles)) {
continue;
}
$classes[] = $className;
}
return $classes;
}
/**
* Creates a FileClassLocator from an array of directories.
*
* @param list<string> $directories
* @param list<string> $excludedDirectories Directories to exclude from the search.
* @param string $fileExtension The file extension to look for (default is '.php').
*
* @throws MappingException if any of the directories are not valid.
*/
public static function createFromDirectories(
array $directories,
array $excludedDirectories = [],
string $fileExtension = '.php',
): self {
$filesIterator = new AppendIterator();
foreach ($directories as $directory) {
if (! is_dir($directory)) {
throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($directory);
}
/** @var Iterator<array-key,SplFileInfo> $iterator */
$iterator = new RegexIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY,
),
sprintf('/%s$/', preg_quote($fileExtension, '/')),
RegexIterator::MATCH,
);
$filesIterator->append($iterator);
}
if ($excludedDirectories !== []) {
$excludedDirectories = array_map(
// realpath() returns false if the file is in a phar archive
// @phpstan-ignore ternary.shortNotAllowed (false is the only falsy value realpath() may return)
static fn (string $dir): string => str_replace('\\', '/', realpath($dir) ?: $dir),
$excludedDirectories,
);
$filesIterator = new CallbackFilterIterator(
$filesIterator,
static function (SplFileInfo $file) use ($excludedDirectories): bool {
// getRealPath() returns false if the file is in a phar archive
// @phpstan-ignore ternary.shortNotAllowed (false is the only falsy value getRealPath() may return)
$sourceFile = str_replace('\\', '/', $file->getRealPath() ?: $file->getPathname());
foreach ($excludedDirectories as $excludedDirectory) {
if (str_starts_with($sourceFile, $excludedDirectory)) {
return false;
}
}
return true;
},
);
}
return new self($filesIterator);
}
}

View File

@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\MappingException;
use function array_keys;
use function array_unique;
use function array_values;
use function is_file;
use function str_replace;
/**
* Base driver for file-based metadata drivers.
*
* A file driver operates in a mode where it loads the mapping files of individual
* classes on demand. This requires the user to adhere to the convention of 1 mapping
* file per class and the file names of the mapping files must correspond to the full
* class name, including namespace, with the namespace delimiters '\', replaced by dots '.'.
*
* @template T
*/
abstract class FileDriver implements MappingDriver
{
protected FileLocator $locator;
/**
* @var mixed[]|null
* @phpstan-var array<class-string, T>|null
*/
protected array|null $classCache = null;
protected string $globalBasename = '';
/**
* Initializes a new FileDriver that looks in the given path(s) for mapping
* documents and operates in the specified operating mode.
*
* @param string|array<int, string>|FileLocator $locator A FileLocator or one/multiple paths
* where mapping documents can be found.
*/
public function __construct(string|array|FileLocator $locator, string|null $fileExtension = null)
{
if ($locator instanceof FileLocator) {
$this->locator = $locator;
} else {
$this->locator = new DefaultFileLocator((array) $locator, $fileExtension);
}
}
/** Sets the global basename. */
public function setGlobalBasename(string $file): void
{
$this->globalBasename = $file;
}
/** Retrieves the global basename. */
public function getGlobalBasename(): string
{
return $this->globalBasename;
}
/**
* 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.
*
* @phpstan-param class-string $className
*
* @return T The element of schema meta data.
*
* @throws MappingException
*/
public function getElement(string $className): mixed
{
if ($this->classCache === null) {
$this->initialize();
}
if (isset($this->classCache[$className])) {
return $this->classCache[$className];
}
$result = $this->loadMappingFile($this->locator->findMappingFile($className));
if (! isset($result[$className])) {
throw MappingException::invalidMappingFile(
$className,
str_replace('\\', '.', $className) . $this->locator->getFileExtension(),
);
}
$this->classCache[$className] = $result[$className];
return $result[$className];
}
public function isTransient(string $className): bool
{
if ($this->classCache === null) {
$this->initialize();
}
if (isset($this->classCache[$className])) {
return false;
}
return ! $this->locator->fileExists($className);
}
/**
* {@inheritDoc}
*/
public function getAllClassNames(): array
{
if ($this->classCache === null) {
$this->initialize();
}
if ($this->classCache === []) {
return $this->locator->getAllClassNames($this->globalBasename);
}
/** @phpstan-var array<class-string, ClassMetadata<object>> $classCache */
$classCache = $this->classCache;
/** @var list<class-string> $keys */
$keys = array_keys($classCache);
return array_values(array_unique([...$keys, ...$this->locator->getAllClassNames($this->globalBasename)]));
}
/**
* Loads a mapping file with the given name and returns a map
* from class/entity names to their corresponding file driver elements.
*
* @param string $file The mapping file to load.
*
* @return mixed[]
* @phpstan-return array<class-string, T>
*/
abstract protected function loadMappingFile(string $file): array;
/**
* Initializes the class cache from all the global files.
*
* Using this feature adds a substantial performance hit to file drivers as
* more metadata has to be loaded into memory than might actually be
* necessary. This may not be relevant to scenarios where caching of
* metadata is in place, however hits very hard in scenarios where no
* caching is used.
*/
protected function initialize(): void
{
$this->classCache = [];
if ($this->globalBasename === '') {
return;
}
foreach ($this->locator->getPaths() as $path) {
$file = $path . '/' . $this->globalBasename . $this->locator->getFileExtension();
if (! is_file($file)) {
continue;
}
$this->classCache = [...$this->classCache, ...$this->loadMappingFile($file)];
}
}
/** Retrieves the locator used to discover mapping files by className. */
public function getLocator(): FileLocator
{
return $this->locator;
}
/** Sets the locator used to discover mapping files by className. */
public function setLocator(FileLocator $locator): void
{
$this->locator = $locator;
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
/**
* Locates the file that contains the metadata information for a given class name.
*
* This behavior is independent of the actual content of the file. It just detects
* the file which is responsible for the given class name.
*/
interface FileLocator
{
/** Locates mapping file for the given class name. */
public function findMappingFile(string $className): string;
/**
* Gets all class names that are found with this file locator.
*
* @param string $globalBasename Passed to allow excluding the basename.
*
* @return array<int, string>
* @phpstan-return list<class-string>
*/
public function getAllClassNames(string $globalBasename): array;
/** Checks if a file can be found for this class name. */
public function fileExists(string $className): bool;
/**
* Gets all the paths that this file locator looks for mapping files.
*
* @return array<int, string>
*/
public function getPaths(): array;
/** Gets the file extension that mapping files are suffixed with. */
public function getFileExtension(): string|null;
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
use Doctrine\Persistence\Mapping\ClassMetadata;
/**
* Contract for metadata drivers.
*/
interface MappingDriver
{
/**
* Loads the metadata for the specified class into the provided container.
*
* @phpstan-param class-string<T> $className
* @phpstan-param ClassMetadata<T> $metadata
*
* @template T of object
*/
public function loadMetadataForClass(string $className, ClassMetadata $metadata): void;
/**
* Gets the names of all mapped classes known to this driver.
*
* @return array<int, string> The names of all mapped classes known to this driver.
* @phpstan-return list<class-string>
*/
public function getAllClassNames(): array;
/**
* 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.
*
* @phpstan-param class-string $className
*/
public function isTransient(string $className): bool;
}

View File

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\MappingException;
use function array_keys;
use function spl_object_id;
use function str_starts_with;
/**
* The DriverChain allows you to add multiple other mapping drivers for
* certain namespaces.
*/
class MappingDriverChain implements MappingDriver
{
/**
* The default driver.
*/
private MappingDriver|null $defaultDriver = null;
/** @var array<string, MappingDriver> */
private array $drivers = [];
/** Gets the default driver. */
public function getDefaultDriver(): MappingDriver|null
{
return $this->defaultDriver;
}
/** Set the default driver. */
public function setDefaultDriver(MappingDriver $driver): void
{
$this->defaultDriver = $driver;
}
/** Adds a nested driver. */
public function addDriver(MappingDriver $nestedDriver, string $namespace): void
{
$this->drivers[$namespace] = $nestedDriver;
}
/**
* Gets the array of nested drivers.
*
* @return array<string, MappingDriver> $drivers
*/
public function getDrivers(): array
{
return $this->drivers;
}
public function loadMetadataForClass(string $className, ClassMetadata $metadata): void
{
foreach ($this->drivers as $namespace => $driver) {
if (str_starts_with($className, $namespace)) {
$driver->loadMetadataForClass($className, $metadata);
return;
}
}
if ($this->defaultDriver !== null) {
$this->defaultDriver->loadMetadataForClass($className, $metadata);
return;
}
throw MappingException::classNotFoundInNamespaces($className, array_keys($this->drivers));
}
/**
* {@inheritDoc}
*/
public function getAllClassNames(): array
{
$classNames = [];
$driverClasses = [];
foreach ($this->drivers as $namespace => $driver) {
$oid = spl_object_id($driver);
if (! isset($driverClasses[$oid])) {
$driverClasses[$oid] = $driver->getAllClassNames();
}
foreach ($driverClasses[$oid] as $className) {
if (! str_starts_with($className, $namespace)) {
continue;
}
$classNames[$className] = true;
}
}
if ($this->defaultDriver !== null) {
foreach ($this->defaultDriver->getAllClassNames() as $className) {
$classNames[$className] = true;
}
}
return array_keys($classNames);
}
public function isTransient(string $className): bool
{
foreach ($this->drivers as $namespace => $driver) {
if (str_starts_with($className, $namespace)) {
return $driver->isTransient($className);
}
}
if ($this->defaultDriver !== null) {
return $this->defaultDriver->isTransient($className);
}
return true;
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
use Doctrine\Persistence\Mapping\ClassMetadata;
/**
* The PHPDriver includes php files which just populate ClassMetadataInfo
* instances with plain PHP code.
*
* @template-extends FileDriver<ClassMetadata<object>>
*/
class PHPDriver extends FileDriver
{
/** @phpstan-var ClassMetadata<object> */
protected ClassMetadata $metadata;
/** @param string|array<int, string>|FileLocator $locator */
public function __construct(string|array|FileLocator $locator)
{
parent::__construct($locator, '.php');
}
public function loadMetadataForClass(string $className, ClassMetadata $metadata): void
{
$this->metadata = $metadata;
$this->loadMappingFile($this->locator->findMappingFile($className));
}
/**
* {@inheritDoc}
*/
protected function loadMappingFile(string $file): array
{
$metadata = $this->metadata;
include $file;
return [$metadata->getName() => $metadata];
}
}

View File

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\MappingException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use function array_unique;
use function get_declared_classes;
use function in_array;
use function is_dir;
use function method_exists;
use function realpath;
/**
* The StaticPHPDriver calls a static loadMetadata() method on your entity
* classes where you can manually populate the ClassMetadata instance.
*/
class StaticPHPDriver implements MappingDriver
{
/**
* Paths of entity directories.
*
* @var array<int, string>
*/
private array $paths = [];
/**
* Map of all class names.
*
* @var array<int, string>
* @phpstan-var list<class-string>
*/
private array|null $classNames = null;
/** @param array<int, string>|string $paths */
public function __construct(array|string $paths)
{
$this->addPaths((array) $paths);
}
/** @param array<int, string> $paths */
public function addPaths(array $paths): void
{
$this->paths = array_unique([...$this->paths, ...$paths]);
}
public function loadMetadataForClass(string $className, ClassMetadata $metadata): void
{
$className::loadMetadata($metadata);
}
/**
* {@inheritDoc}
*
* @todo Same code exists in ColocatedMappingDriver, should we re-use it
* somehow or not worry about it?
*/
public function getAllClassNames(): array
{
if ($this->classNames !== null) {
return $this->classNames;
}
if ($this->paths === []) {
throw MappingException::pathRequiredForDriver(static::class);
}
$classes = [];
$includedFiles = [];
foreach ($this->paths as $path) {
if (! is_dir($path)) {
throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path);
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::LEAVES_ONLY,
);
foreach ($iterator as $file) {
if ($file->getBasename('.php') === $file->getBasename()) {
continue;
}
$sourceFile = realpath($file->getPathName());
require_once $sourceFile;
$includedFiles[] = $sourceFile;
}
}
$declared = get_declared_classes();
foreach ($declared as $className) {
$rc = new ReflectionClass($className);
$sourceFile = $rc->getFileName();
if (! in_array($sourceFile, $includedFiles, true) || $this->isTransient($className)) {
continue;
}
$classes[] = $className;
}
$this->classNames = $classes;
return $classes;
}
public function isTransient(string $className): bool
{
return ! method_exists($className, 'loadMetadata');
}
}

View File

@ -0,0 +1,245 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping\Driver;
use Doctrine\Persistence\Mapping\MappingException;
use InvalidArgumentException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;
use function array_keys;
use function assert;
use function is_dir;
use function is_file;
use function is_int;
use function realpath;
use function sprintf;
use function str_replace;
use function str_starts_with;
use function strlen;
use function strrpos;
use function strtr;
use function substr;
use const DIRECTORY_SEPARATOR;
/**
* The Symfony File Locator makes a simplifying assumptions compared
* to the DefaultFileLocator. By assuming paths only contain entities of a certain
* namespace the mapping files consists of the short classname only.
*/
class SymfonyFileLocator implements FileLocator
{
/**
* The paths where to look for mapping files.
*
* @var array<int, string>
*/
protected array $paths = [];
/**
* A map of mapping directory path to namespace prefix used to expand class shortnames.
*
* @var array<string, string>
*/
protected array $prefixes = [];
/** File extension that is searched for. */
protected string|null $fileExtension;
/**
* Represents PHP namespace delimiters when looking for files
*/
private readonly string $nsSeparator;
/**
* @param array<string, string> $prefixes
* @param string $nsSeparator String which would be used when converting FQCN
* to filename and vice versa. Should not be empty
*/
public function __construct(
array $prefixes,
string $fileExtension = '',
string $nsSeparator = '.',
) {
$this->addNamespacePrefixes($prefixes);
$this->fileExtension = $fileExtension;
if ($nsSeparator === '') {
throw new InvalidArgumentException('Namespace separator should not be empty');
}
$this->nsSeparator = $nsSeparator;
}
/**
* Adds Namespace Prefixes.
*
* @param array<string, string> $prefixes
*/
public function addNamespacePrefixes(array $prefixes): void
{
$this->prefixes = [...$this->prefixes, ...$prefixes];
$this->paths = [...$this->paths, ...array_keys($prefixes)];
}
/**
* Gets Namespace Prefixes.
*
* @return string[]
*/
public function getNamespacePrefixes(): array
{
return $this->prefixes;
}
/**
* {@inheritDoc}
*/
public function getPaths(): array
{
return $this->paths;
}
public function getFileExtension(): string|null
{
return $this->fileExtension;
}
/**
* Sets the file extension used to look for mapping files under.
*
* @param string $fileExtension The file extension to set.
*/
public function setFileExtension(string $fileExtension): void
{
$this->fileExtension = $fileExtension;
}
public function fileExists(string $className): bool
{
$defaultFileName = str_replace('\\', $this->nsSeparator, $className) . $this->fileExtension;
foreach ($this->paths as $path) {
if (! isset($this->prefixes[$path])) {
// global namespace class
if (is_file($path . DIRECTORY_SEPARATOR . $defaultFileName)) {
return true;
}
continue;
}
$prefix = $this->prefixes[$path];
if (! str_starts_with($className, $prefix . '\\')) {
continue;
}
$filename = $path . '/' . strtr(substr($className, strlen($prefix) + 1), '\\', $this->nsSeparator) . $this->fileExtension;
if (is_file($filename)) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
public function getAllClassNames(string|null $globalBasename = null): array
{
if ($this->paths === []) {
return [];
}
$classes = [];
foreach ($this->paths as $path) {
if (! is_dir($path)) {
throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path);
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::LEAVES_ONLY,
);
foreach ($iterator as $file) {
$fileName = $file->getBasename($this->fileExtension);
if ($fileName === $file->getBasename() || $fileName === $globalBasename) {
continue;
}
// NOTE: All files found here means classes are not transient!
if (isset($this->prefixes[$path])) {
// Calculate namespace suffix for given prefix as a relative path from basepath to file path
$nsSuffix = strtr(
substr($this->realpath($file->getPath()), strlen($this->realpath($path))),
$this->nsSeparator,
'\\',
);
/** @phpstan-var class-string */
$class = $this->prefixes[$path] . str_replace(DIRECTORY_SEPARATOR, '\\', $nsSuffix) . '\\' . str_replace($this->nsSeparator, '\\', $fileName);
} else {
/** @phpstan-var class-string */
$class = str_replace($this->nsSeparator, '\\', $fileName);
}
$classes[] = $class;
}
}
return $classes;
}
public function findMappingFile(string $className): string
{
$defaultFileName = str_replace('\\', $this->nsSeparator, $className) . $this->fileExtension;
foreach ($this->paths as $path) {
if (! isset($this->prefixes[$path])) {
if (is_file($path . DIRECTORY_SEPARATOR . $defaultFileName)) {
return $path . DIRECTORY_SEPARATOR . $defaultFileName;
}
continue;
}
$prefix = $this->prefixes[$path];
if (! str_starts_with($className, $prefix . '\\')) {
continue;
}
$filename = $path . '/' . strtr(substr($className, strlen($prefix) + 1), '\\', $this->nsSeparator) . $this->fileExtension;
if (is_file($filename)) {
return $filename;
}
}
$pos = strrpos($className, '\\');
assert(is_int($pos));
throw MappingException::mappingFileNotFound(
$className,
substr($className, $pos + 1) . $this->fileExtension,
);
}
private function realpath(string $path): string
{
$realpath = realpath($path);
if ($realpath === false) {
throw new RuntimeException(sprintf('Could not get realpath for %s', $path));
}
return $realpath;
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping;
use Exception;
use function implode;
use function sprintf;
/**
* A MappingException indicates that something is wrong with the mapping setup.
*/
class MappingException extends Exception
{
/** @param array<int, string> $namespaces */
public static function classNotFoundInNamespaces(
string $className,
array $namespaces,
): self {
return new self(sprintf(
"The class '%s' was not found in the chain configured namespaces %s",
$className,
implode(', ', $namespaces),
));
}
/** @param class-string $driverClassName */
public static function pathRequiredForDriver(string $driverClassName): self
{
return new self(sprintf(
'Specifying source file paths to your entities is required when using %s to retrieve all class names.',
$driverClassName,
));
}
public static function fileMappingDriversRequireConfiguredDirectoryPath(
string|null $path = null,
): self {
if ($path !== null) {
$path = '[' . $path . ']';
}
return new self(sprintf(
'File mapping drivers must have a valid directory path, ' .
'however the given path %s seems to be incorrect!',
(string) $path,
));
}
public static function mappingFileNotFound(string $entityName, string $fileName): self
{
return new self(sprintf(
"No mapping file found named '%s' for class '%s'.",
$fileName,
$entityName,
));
}
public static function invalidMappingFile(string $entityName, string $fileName): self
{
return new self(sprintf(
"Invalid mapping file '%s' for class '%s'.",
$fileName,
$entityName,
));
}
public static function nonExistingClass(string $className): self
{
return new self(sprintf("Class '%s' does not exist", $className));
}
/** @param class-string $className */
public static function classIsAnonymous(string $className): self
{
return new self(sprintf('Class "%s" is anonymous', $className));
}
}

View File

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

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping;
use ReflectionClass;
use ReflectionProperty;
/**
* Very simple reflection service abstraction.
*
* This is required inside metadata layers that may require either
* static or runtime reflection.
*/
interface ReflectionService
{
/**
* Returns an array of the parent classes (not interfaces) for the given class.
*
* @phpstan-param class-string $class
*
* @return string[]
* @phpstan-return class-string[]
*
* @throws MappingException
*/
public function getParentClasses(string $class): array;
/**
* Returns the shortname of a class.
*
* @phpstan-param class-string $class
*/
public function getClassShortName(string $class): string;
/** @phpstan-param class-string $class */
public function getClassNamespace(string $class): string;
/**
* Returns a reflection class instance or null.
*
* @phpstan-param class-string<T> $class
*
* @phpstan-return ReflectionClass<T>
*
* @template T of object
*/
public function getClass(string $class): ReflectionClass;
/**
* Returns an accessible property or null.
*
* @phpstan-param class-string $class
*/
public function getAccessibleProperty(string $class, string $property): ReflectionProperty|null;
/**
* Checks if the class have a public method with the given name.
*
* @phpstan-param class-string $class
*/
public function hasPublicMethod(string $class, string $method): bool;
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Mapping;
use Doctrine\Persistence\Reflection\RuntimeReflectionProperty;
use Doctrine\Persistence\Reflection\TypedNoDefaultReflectionProperty;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use function array_key_exists;
use function assert;
use function class_exists;
use function class_parents;
use function phpversion;
use function version_compare;
/**
* PHP Runtime Reflection Service.
*/
class RuntimeReflectionService implements ReflectionService
{
private readonly bool $supportsTypedPropertiesWorkaround;
public function __construct()
{
$this->supportsTypedPropertiesWorkaround = version_compare(phpversion(), '7.4.0') >= 0;
}
/**
* {@inheritDoc}
*/
public function getParentClasses(string $class): array
{
if (! class_exists($class)) {
throw MappingException::nonExistingClass($class);
}
$parents = class_parents($class);
assert($parents !== false);
return $parents;
}
public function getClassShortName(string $class): string
{
$reflectionClass = new ReflectionClass($class);
return $reflectionClass->getShortName();
}
public function getClassNamespace(string $class): string
{
$reflectionClass = new ReflectionClass($class);
return $reflectionClass->getNamespaceName();
}
/**
* @phpstan-param class-string<T> $class
*
* @phpstan-return ReflectionClass<T>
*
* @template T of object
*/
public function getClass(string $class): ReflectionClass
{
return new ReflectionClass($class);
}
public function getAccessibleProperty(string $class, string $property): RuntimeReflectionProperty
{
$reflectionProperty = new RuntimeReflectionProperty($class, $property);
if ($this->supportsTypedPropertiesWorkaround && ! array_key_exists($property, $this->getClass($class)->getDefaultProperties())) {
$reflectionProperty = new TypedNoDefaultReflectionProperty($class, $property);
}
return $reflectionProperty;
}
public function hasPublicMethod(string $class, string $method): bool
{
try {
$reflectionMethod = new ReflectionMethod($class, $method);
} catch (ReflectionException) {
return false;
}
return $reflectionMethod->isPublic();
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence;
/**
* Interface for classes that notify event listeners of changes to their managed properties.
*
* This interface is implemented by objects that manually want to notify their object manager or
* other listeners when properties change, instead of relying on the object manager to compute
* property changes itself when changes are to be persisted.
*/
interface NotifyPropertyChanged
{
/** Adds a listener that wants to be notified about property changes. */
public function addPropertyChangedListener(PropertyChangedListener $listener): void;
}

View File

@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\ClassMetadataFactory;
/** Contract for a Doctrine persistence layer ObjectManager class to implement. */
interface ObjectManager
{
/**
* Finds an object by its identifier.
*
* This is just a convenient shortcut for getRepository($className)->find($id).
*
* @param string $className The class name of the object to find.
* @param mixed $id The identity of the object to find.
* @phpstan-param class-string<T> $className
*
* @return object|null The found object.
* @phpstan-return T|null
*
* @template T of object
*/
public function find(string $className, mixed $id): object|null;
/**
* Tells the ObjectManager to make an instance managed and persistent.
*
* The object will be entered into the database as a result of the flush operation.
*
* NOTE: The persist operation always considers objects that are not yet known to
* this ObjectManager as NEW. Do not pass detached objects to the persist operation.
*
* @param object $object The instance to make managed and persistent.
*/
public function persist(object $object): void;
/**
* Removes an object instance.
*
* A removed object will be removed from the database as a result of the flush operation.
*
* @param object $object The object instance to remove.
*/
public function remove(object $object): void;
/**
* Clears the ObjectManager. All objects that are currently managed
* by this ObjectManager become detached.
*/
public function clear(): void;
/**
* Detaches an object from the ObjectManager, causing a managed object to
* become detached. Unflushed changes made to the object if any
* (including removal of the object), will not be synchronized to the database.
* Objects which previously referenced the detached object will continue to
* reference it.
*
* @param object $object The object to detach.
*/
public function detach(object $object): void;
/**
* Refreshes the persistent state of an object from the database,
* overriding any local changes that have not yet been persisted.
*
* @param object $object The object to refresh.
*/
public function refresh(object $object): void;
/**
* Flushes all changes to objects that have been queued up to now to the database.
* This effectively synchronizes the in-memory state of managed objects with the
* database.
*/
public function flush(): void;
/**
* Gets the repository for a class.
*
* @phpstan-param class-string<T> $className
*
* @phpstan-return ObjectRepository<T>
*
* @template T of object
*/
public function getRepository(string $className): ObjectRepository;
/**
* Returns the ClassMetadata descriptor for a class.
*
* The class name must be the fully-qualified class name without a leading backslash
* (as it is returned by get_class($obj)).
*
* @phpstan-param class-string<T> $className
*
* @phpstan-return ClassMetadata<T>
*
* @template T of object
*/
public function getClassMetadata(string $className): ClassMetadata;
/**
* Gets the metadata factory used to gather the metadata of classes.
*
* @phpstan-return ClassMetadataFactory<ClassMetadata<object>>
*/
public function getMetadataFactory(): ClassMetadataFactory;
/**
* Helper method to initialize a lazy loading proxy or persistent collection.
*
* This method is a no-op for other objects.
*/
public function initializeObject(object $obj): void;
/** Helper method to check whether a lazy loading proxy or persistent collection has been initialized. */
public function isUninitializedObject(mixed $value): bool;
/**
* Checks if the object is part of the current UnitOfWork and therefore managed.
*/
public function contains(object $object): bool;
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\ClassMetadataFactory;
/**
* Base class to simplify ObjectManager decorators
*
* @template-covariant TObjectManager of ObjectManager
*/
abstract class ObjectManagerDecorator implements ObjectManager
{
/** @var TObjectManager */
protected ObjectManager $wrapped;
/**
* {@inheritDoc}
*/
public function find(string $className, $id): object|null
{
return $this->wrapped->find($className, $id);
}
public function persist(object $object): void
{
$this->wrapped->persist($object);
}
public function remove(object $object): void
{
$this->wrapped->remove($object);
}
public function clear(): void
{
$this->wrapped->clear();
}
public function detach(object $object): void
{
$this->wrapped->detach($object);
}
public function refresh(object $object): void
{
$this->wrapped->refresh($object);
}
public function flush(): void
{
$this->wrapped->flush();
}
public function getRepository(string $className): ObjectRepository
{
return $this->wrapped->getRepository($className);
}
public function getClassMetadata(string $className): ClassMetadata
{
return $this->wrapped->getClassMetadata($className);
}
/** @phpstan-return ClassMetadataFactory<ClassMetadata<object>> */
public function getMetadataFactory(): ClassMetadataFactory
{
return $this->wrapped->getMetadataFactory();
}
public function initializeObject(object $obj): void
{
$this->wrapped->initializeObject($obj);
}
public function isUninitializedObject(mixed $value): bool
{
return $this->wrapped->isUninitializedObject($value);
}
public function contains(object $object): bool
{
return $this->wrapped->contains($object);
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence;
use UnexpectedValueException;
/**
* Contract for a Doctrine persistence layer ObjectRepository class to implement.
*
* @template-covariant T of object
*/
interface ObjectRepository
{
/**
* Finds an object by its primary key / identifier.
*
* @param mixed $id The identifier.
*
* @return object|null The object.
* @phpstan-return T|null
*/
public function find(mixed $id): object|null;
/**
* Finds all objects in the repository.
*
* @return array<int, object> The objects.
* @phpstan-return T[]
*/
public function findAll(): array;
/**
* Finds objects by a set of criteria.
*
* Optionally sorting and limiting details can be passed. An implementation may throw
* an UnexpectedValueException if certain values of the sorting or limiting details are
* not supported.
*
* @param array<string, mixed> $criteria
* @param array<string, string>|null $orderBy
* @phpstan-param array<string, 'asc'|'desc'|'ASC'|'DESC'>|null $orderBy
*
* @return array<int, object> The objects.
* @phpstan-return T[]
*
* @throws UnexpectedValueException
*/
public function findBy(
array $criteria,
array|null $orderBy = null,
int|null $limit = null,
int|null $offset = null,
): array;
/**
* Finds a single object by a set of criteria.
*
* @param array<string, mixed> $criteria The criteria.
*
* @return object|null The object.
* @phpstan-return T|null
*/
public function findOneBy(array $criteria): object|null;
/**
* Returns the class name of the object managed by the repository.
*
* @phpstan-return class-string<T>
*/
public function getClassName(): string;
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence;
/**
* Contract for classes that are potential listeners of a {@see NotifyPropertyChanged}
* implementor.
*/
interface PropertyChangedListener
{
/**
* Collect information about a property change.
*
* @param object $sender The object on which the property changed.
* @param string $propertyName The name of the property that changed.
* @param mixed $oldValue The old value of the property that changed.
* @param mixed $newValue The new value of the property that changed.
*/
public function propertyChanged(object $sender, string $propertyName, mixed $oldValue, mixed $newValue): void;
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence;
/**
* Interface for proxy classes.
*
* @template T of object
*/
interface Proxy
{
/**
* Marker for Proxy class names.
*/
public const MARKER = '__CG__';
/**
* Length of the proxy marker.
*/
public const MARKER_LENGTH = 6;
/**
* Initializes this proxy if its not yet initialized.
*
* Acts as a no-op if already initialized.
*/
public function __load(): void;
/** Returns whether this proxy is initialized or not. */
public function __isInitialized(): bool;
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Reflection;
use BackedEnum;
use ReflectionProperty;
use function array_map;
use function is_array;
use function reset;
/**
* PHP Enum Reflection Property - special override for backed enums.
*/
class EnumReflectionProperty extends ReflectionProperty
{
/** @param class-string<BackedEnum> $enumType */
public function __construct(private readonly ReflectionProperty $originalReflectionProperty, private readonly string $enumType)
{
parent::__construct($originalReflectionProperty->class, $originalReflectionProperty->name);
}
/**
* {@inheritDoc}
*
* Converts enum instance to its value.
*
* @param object|null $object
*
* @return int|string|int[]|string[]|null
*/
public function getValue($object = null): int|string|array|null
{
if ($object === null) {
return null;
}
$enum = $this->originalReflectionProperty->getValue($object);
if ($enum === null) {
return null;
}
return $this->fromEnum($enum);
}
/**
* Converts enum value to enum instance.
*
* @param object|null $object
*/
public function setValue(mixed $object, mixed $value = null): void
{
if ($value !== null) {
$value = $this->toEnum($value);
}
$this->originalReflectionProperty->setValue($object, $value);
}
/**
* @param BackedEnum|BackedEnum[] $enum
*
* @return ($enum is BackedEnum ? (string|int) : (string[]|int[]))
*/
private function fromEnum(BackedEnum|array $enum)
{
if (is_array($enum)) {
return array_map(static fn (BackedEnum $enum) => $enum->value, $enum);
}
return $enum->value;
}
/**
* @param int|string|int[]|string[]|BackedEnum|BackedEnum[] $value
*
* @return ($value is int|string|BackedEnum ? BackedEnum : BackedEnum[])
*/
private function toEnum(int|string|array|BackedEnum $value)
{
if ($value instanceof BackedEnum) {
return $value;
}
if (is_array($value)) {
$v = reset($value);
if ($v instanceof BackedEnum) {
return $value;
}
return array_map([$this->enumType, 'from'], $value);
}
return $this->enumType::from($value);
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Reflection;
use Doctrine\Persistence\Proxy;
use ReflectionProperty;
use function ltrim;
use function method_exists;
/**
* PHP Runtime Reflection Property.
*
* Avoids triggering lazy loading if the provided object
* is a {@see \Doctrine\Persistence\Proxy}.
*/
class RuntimeReflectionProperty extends ReflectionProperty
{
private readonly string $key;
/** @param class-string $class */
public function __construct(string $class, string $name)
{
parent::__construct($class, $name);
$this->key = $this->isPrivate() ? "\0" . ltrim($class, '\\') . "\0" . $name : ($this->isProtected() ? "\0*\0" . $name : $name);
}
public function getValue(object|null $object = null): mixed
{
if ($object === null) {
return parent::getValue($object);
}
return ((array) $object)[$this->key] ?? null;
}
/**
* {@inheritDoc}
*
* @param object|null $object
*/
public function setValue(mixed $object, mixed $value = null): void
{
if (! ($object instanceof Proxy && ! $object->__isInitialized())) {
parent::setValue($object, $value);
return;
}
if (! method_exists($object, '__setInitialized')) {
return;
}
$object->__setInitialized(true);
parent::setValue($object, $value);
$object->__setInitialized(false);
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Doctrine\Persistence\Reflection;
use Closure;
use function assert;
/**
* PHP Typed No Default Reflection Property - special override for typed properties without a default value.
*/
class TypedNoDefaultReflectionProperty extends RuntimeReflectionProperty
{
/**
* {@inheritDoc}
*
* Checks that a typed property is initialized before accessing its value.
* This is necessary to avoid PHP error "Error: Typed property must not be accessed before initialization".
* Should be used only for reflecting typed properties without a default value.
*/
public function getValue(object|null $object = null): mixed
{
return $object !== null && $this->isInitialized($object) ? parent::getValue($object) : null;
}
/**
* {@inheritDoc}
*
* Works around the problem with setting typed no default properties to
* NULL which is not supported, instead unset() to uninitialize.
*
* @link https://github.com/doctrine/orm/issues/7999
*
* @param object|null $object
*/
public function setValue(mixed $object, mixed $value = null): void
{
if ($value === null && $this->hasType() && ! $this->getType()->allowsNull()) {
$propertyName = $this->getName();
$unsetter = function () use ($propertyName): void {
unset($this->$propertyName);
};
$unsetter = $unsetter->bindTo($object, $this->getDeclaringClass()->getName());
assert($unsetter instanceof Closure);
$unsetter();
return;
}
parent::setValue($object, $value);
}
}