├── LICENSE ├── README.md ├── UPGRADE.md ├── composer.json └── src └── Persistence ├── AbstractManagerRegistry.php ├── ConnectionRegistry.php ├── Event ├── LifecycleEventArgs.php ├── LoadClassMetadataEventArgs.php ├── ManagerEventArgs.php ├── OnClearEventArgs.php └── PreUpdateEventArgs.php ├── ManagerRegistry.php ├── Mapping ├── AbstractClassMetadataFactory.php ├── ClassMetadata.php ├── ClassMetadataFactory.php ├── Driver │ ├── ColocatedMappingDriver.php │ ├── DefaultFileLocator.php │ ├── FileDriver.php │ ├── FileLocator.php │ ├── MappingDriver.php │ ├── MappingDriverChain.php │ ├── PHPDriver.php │ ├── StaticPHPDriver.php │ └── SymfonyFileLocator.php ├── MappingException.php ├── ProxyClassNameResolver.php ├── ReflectionService.php └── RuntimeReflectionService.php ├── NotifyPropertyChanged.php ├── ObjectManager.php ├── ObjectManagerDecorator.php ├── ObjectRepository.php ├── PropertyChangedListener.php ├── Proxy.php └── Reflection ├── EnumReflectionProperty.php ├── RuntimeReflectionProperty.php └── TypedNoDefaultReflectionProperty.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2015 Doctrine Project 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctrine Persistence 2 | 3 | [![GitHub Actions][GA 4.0 image]][GA 4.0] 4 | [![Code Coverage][Coverage 4.0 image]][CodeCov 4.0] 5 | 6 | The Doctrine Persistence project is a library that provides common abstractions for object mapper persistence. 7 | 8 | ## More resources: 9 | 10 | * [Website](https://www.doctrine-project.org/) 11 | * [Documentation](https://www.doctrine-project.org/projects/doctrine-persistence/en/latest/index.html) 12 | * [Downloads](https://github.com/doctrine/persistence/releases) 13 | 14 | [Coverage 4.0 image]: https://codecov.io/gh/doctrine/persistence/branch/4.0.x/graph/badge.svg 15 | [CodeCov 4.0]: https://codecov.io/gh/doctrine/persistence/branch/4.0.x 16 | [GA 4.0 image]: https://github.com/doctrine/persistence/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0.x 17 | [GA 4.0]: https://github.com/doctrine/persistence/actions/workflows/continuous-integration.yml?branch=4.0.x 18 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | Note about upgrading: Doctrine uses static and runtime mechanisms to raise 2 | awareness about deprecated code. 3 | 4 | - Use of `@deprecated` docblock that is detected by IDEs (like PHPStorm) or 5 | Static Analysis tools (like Psalm, phpstan) 6 | - Use of our low-overhead runtime deprecation API, details: 7 | https://github.com/doctrine/deprecations/ 8 | 9 | # Upgrade to 4.0 10 | 11 | ## BC Break: Removed `StaticReflectionService` 12 | 13 | The class `Doctrine\Persistence\Mapping\StaticReflectionService` is removed 14 | without replacement. 15 | 16 | ## BC Break: Narrowed `ReflectionService::getClass()` return type 17 | 18 | The return type of `ReflectionService::getClass()` has been narrowed so that 19 | `null` is no longer a valid return value. 20 | 21 | ## BC Break: Added `ObjectManager::isUninitializedObject()` 22 | 23 | Classes implementing `Doctrine\Persistence\ObjectManager` must implement this 24 | new method. 25 | 26 | ## BC Break: Added type declarations 27 | 28 | The code base is now fully typed, meaning properties, parameters and return 29 | type declarations have been added to all types. 30 | 31 | ## BC Break: Dropped support for Common proxies 32 | 33 | Proxy objects implementing the `Doctrine\Common\Proxy\Proxy` interface are not 34 | supported anymore. Implement `Doctrine\Persistence\Proxy` instead. 35 | 36 | ## BC Break: Removed deprecated ReflectionProperty overrides 37 | 38 | Deprecated classes have been removed: 39 | 40 | - `Doctrine\Persistence\Reflection\RuntimePublicReflectionProperty` 41 | - `Doctrine\Persistence\Reflection\TypedNoDefaultRuntimePublicReflectionProperty` 42 | 43 | # Upgrade to 3.4 44 | 45 | ## Deprecated `StaticReflectionService` 46 | 47 | The class `Doctrine\Persistence\Mapping\StaticReflectionService` is deprecated 48 | without replacement. 49 | 50 | # Upgrade to 3.3 51 | 52 | ## Added method `ObjectManager::isUninitializedObject()` 53 | 54 | Classes implementing `Doctrine\Persistence\ObjectManager` should implement the new 55 | method. This method will be added to the interface in 4.0. 56 | 57 | # Upgrade to 3.1 58 | 59 | ## Deprecated `RuntimePublicReflectionProperty` 60 | 61 | Use `RuntimeReflectionProperty` instead. 62 | 63 | # Upgrade to 3.0 64 | 65 | ## Removed `OnClearEventArgs::clearsAllEntities()` and `OnClearEventArgs::getEntityClass()` 66 | 67 | These methods only make sense when partially clearing the object manager, which 68 | is no longer possible. 69 | The second argument of the constructor of `OnClearEventArgs` is removed as well. 70 | 71 | ## BC Break: removed `ObjectManagerAware` 72 | 73 | Implement active record style functionality directly in your application, by 74 | using a `postLoad` event. 75 | 76 | ## BC Break: removed `AnnotationDriver` 77 | 78 | Use `ColocatedMappingDriver` instead. 79 | 80 | ## BC Break: Removed `MappingException::pathRequired()` 81 | 82 | Use `MappingException::pathRequiredForDriver()` instead. 83 | 84 | ## BC Break: removed `LifecycleEventArgs::getEntity()` 85 | 86 | Use `LifecycleEventArgs::getObject()` instead. 87 | 88 | ## BC Break: removed support for short namespace aliases 89 | 90 | - `AbstractClassMetadataFactory::getFqcnFromAlias()` is removed. 91 | - `ClassMetadataFactory` methods now require their `$className` argument to be an 92 | actual FQCN. 93 | 94 | ## BC Break: removed `ObjectManager::merge()` 95 | 96 | `ObjectManagerDecorator::merge()` is removed without replacement. 97 | 98 | ## BC Break: removed support for `doctrine/cache` 99 | 100 | Removed support for using doctrine/cache for metadata caching. The 101 | `setCacheDriver` and `getCacheDriver` methods have been removed from 102 | `Doctrine\Persistence\Mapping\AbstractMetadata`. Please use `getCache` and 103 | `setCache` with a PSR-6 implementation instead. 104 | 105 | ## BC Break: changed signatures 106 | 107 | `$objectName` has been dropped from the signature of `ObjectManager::clear()`. 108 | 109 | ```diff 110 | - public function clear($objectName = null) 111 | + public function clear(): void 112 | ``` 113 | 114 | Also, native parameter type declarations have been added on all public APIs. 115 | Native return type declarations have not been added so that it is possible to 116 | implement types compatible with both 2.x and 3.x. 117 | 118 | ## BC Break: Removed `PersistentObject` 119 | 120 | Please implement this functionality directly in your application if you want 121 | ActiveRecord style functionality. 122 | 123 | # Upgrade to 2.5 124 | 125 | ## Deprecated `OnClearEventArgs::clearsAllEntities()` and `OnClearEventArgs::getEntityClass()` 126 | 127 | These methods only make sense when partially clearing the object manager, which 128 | is deprecated. 129 | Passing a second argument to the constructor of `OnClearEventArgs` is 130 | deprecated as well. 131 | 132 | ## Deprecated `ObjectManagerAware` 133 | 134 | Along with deprecating `PersistentObject`, deprecating `ObjectManagerAware` 135 | means deprecating support for active record, which already came with a word of 136 | warning. Please implement this directly in your application with a `postLoad` 137 | event if you need active record style functionality. 138 | 139 | ## Deprecated `MappingException::pathRequired()` 140 | 141 | `MappingException::pathRequiredForDriver()` should be used instead. 142 | 143 | # Upgrade to 2.4 144 | 145 | ## Deprecated `AnnotationDriver` 146 | 147 | Since attributes were introduced in PHP 8.0, annotations are deprecated. 148 | `AnnotationDriver` is an abstract class that is used when implementing concrete 149 | annotation drivers in dependent packages. It is deprecated in favor of using 150 | `ColocatedMappingDriver` to implement both annotation and attribute based 151 | drivers. This will involve implementing `isTransient()` as well as 152 | `__construct()` and `getReader()` to retain backward compatibility. 153 | 154 | # Upgrade to 2.3 155 | 156 | ## Deprecated using short namespace alias syntax in favor of `::class` syntax. 157 | 158 | Before: 159 | 160 | ```php 161 | $objectManager->find('MyPackage:MyClass', $id); 162 | $objectManager->createQuery('SELECT u FROM MyPackage:MyClass'); 163 | ``` 164 | 165 | After: 166 | 167 | ```php 168 | $objectManager->find(MyClass::class, $id); 169 | $objectManager->createQuery('SELECT u FROM '. MyClass::class); 170 | ``` 171 | 172 | # Upgrade to 2.2 173 | 174 | ## Deprecated `doctrine/cache` usage for metadata caching 175 | 176 | The `setCacheDriver` and `getCacheDriver` methods in 177 | `Doctrine\Persistence\Mapping\AbstractMetadata` have been deprecated. Please 178 | use `getCache` and `setCache` with a PSR-6 implementation instead. Note that 179 | even after switching to PSR-6, `getCacheDriver` will return a cache instance 180 | that wraps the PSR-6 cache. Note that if you use a custom implementation of 181 | doctrine/cache, the library may not be able to provide a forward compatibility 182 | layer. The cache implementation MUST extend the 183 | `Doctrine\Common\Cache\CacheProvider` class. 184 | 185 | # Upgrade to 1.2 186 | 187 | ## Deprecated `ObjectManager::merge()` and `ObjectManager::detach()` 188 | 189 | Please handle merge operations in your application, and use 190 | `ObjectManager::clear()` instead. 191 | 192 | ## Deprecated `PersistentObject` 193 | 194 | Please implement this functionality directly in your application if you want 195 | ActiveRecord style functionality. 196 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doctrine/persistence", 3 | "type": "library", 4 | "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", 5 | "keywords": [ 6 | "persistence", 7 | "object", 8 | "mapper", 9 | "orm", 10 | "odm" 11 | ], 12 | "homepage": "https://www.doctrine-project.org/projects/persistence.html", 13 | "license": "MIT", 14 | "authors": [ 15 | {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, 16 | {"name": "Roman Borschel", "email": "roman@code-factory.org"}, 17 | {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}, 18 | {"name": "Jonathan Wage", "email": "jonwage@gmail.com"}, 19 | {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"}, 20 | {"name": "Marco Pivetta", "email": "ocramius@gmail.com"} 21 | ], 22 | "require": { 23 | "php": "^8.1", 24 | "doctrine/event-manager": "^1 || ^2", 25 | "psr/cache": "^1.0 || ^2.0 || ^3.0" 26 | }, 27 | "require-dev": { 28 | "phpstan/phpstan": "1.12.7", 29 | "phpstan/phpstan-phpunit": "^1", 30 | "phpstan/phpstan-strict-rules": "^1.1", 31 | "doctrine/coding-standard": "^12", 32 | "phpunit/phpunit": "^9.6", 33 | "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Doctrine\\Persistence\\": "src/Persistence" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Doctrine\\Tests\\": "tests" 43 | } 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "dealerdirect/phpcodesniffer-composer-installer": true, 48 | "composer/package-versions-deprecated": true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Persistence/AbstractManagerRegistry.php: -------------------------------------------------------------------------------- 1 | $connections 20 | * @param array $managers 21 | * @phpstan-param class-string $proxyInterfaceName 22 | */ 23 | public function __construct( 24 | private readonly string $name, 25 | private array $connections, 26 | private array $managers, 27 | private readonly string $defaultConnection, 28 | private readonly string $defaultManager, 29 | private readonly string $proxyInterfaceName, 30 | ) { 31 | } 32 | 33 | /** 34 | * Fetches/creates the given services. 35 | * 36 | * A service in this context is connection or a manager instance. 37 | * 38 | * @param string $name The name of the service. 39 | * 40 | * @return object The instance of the given service. 41 | */ 42 | abstract protected function getService(string $name): object; 43 | 44 | /** 45 | * Resets the given services. 46 | * 47 | * A service in this context is connection or a manager instance. 48 | * 49 | * @param string $name The name of the service. 50 | */ 51 | abstract protected function resetService(string $name): void; 52 | 53 | /** Gets the name of the registry. */ 54 | public function getName(): string 55 | { 56 | return $this->name; 57 | } 58 | 59 | public function getConnection(string|null $name = null): object 60 | { 61 | if ($name === null) { 62 | $name = $this->defaultConnection; 63 | } 64 | 65 | if (! isset($this->connections[$name])) { 66 | throw new InvalidArgumentException( 67 | sprintf('Doctrine %s Connection named "%s" does not exist.', $this->name, $name), 68 | ); 69 | } 70 | 71 | return $this->getService($this->connections[$name]); 72 | } 73 | 74 | /** 75 | * {@inheritDoc} 76 | */ 77 | public function getConnectionNames(): array 78 | { 79 | return $this->connections; 80 | } 81 | 82 | /** 83 | * {@inheritDoc} 84 | */ 85 | public function getConnections(): array 86 | { 87 | $connections = []; 88 | foreach ($this->connections as $name => $id) { 89 | $connections[$name] = $this->getService($id); 90 | } 91 | 92 | return $connections; 93 | } 94 | 95 | public function getDefaultConnectionName(): string 96 | { 97 | return $this->defaultConnection; 98 | } 99 | 100 | public function getDefaultManagerName(): string 101 | { 102 | return $this->defaultManager; 103 | } 104 | 105 | /** 106 | * {@inheritDoc} 107 | * 108 | * @throws InvalidArgumentException 109 | */ 110 | public function getManager(string|null $name = null): ObjectManager 111 | { 112 | if ($name === null) { 113 | $name = $this->defaultManager; 114 | } 115 | 116 | if (! isset($this->managers[$name])) { 117 | throw new InvalidArgumentException( 118 | sprintf('Doctrine %s Manager named "%s" does not exist.', $this->name, $name), 119 | ); 120 | } 121 | 122 | $service = $this->getService($this->managers[$name]); 123 | assert($service instanceof ObjectManager); 124 | 125 | return $service; 126 | } 127 | 128 | public function getManagerForClass(string $class): ObjectManager|null 129 | { 130 | $proxyClass = new ReflectionClass($class); 131 | if ($proxyClass->isAnonymous()) { 132 | return null; 133 | } 134 | 135 | if ($proxyClass->implementsInterface($this->proxyInterfaceName)) { 136 | $parentClass = $proxyClass->getParentClass(); 137 | 138 | if ($parentClass === false) { 139 | return null; 140 | } 141 | 142 | $class = $parentClass->getName(); 143 | } 144 | 145 | foreach ($this->managers as $id) { 146 | $manager = $this->getService($id); 147 | assert($manager instanceof ObjectManager); 148 | 149 | if (! $manager->getMetadataFactory()->isTransient($class)) { 150 | return $manager; 151 | } 152 | } 153 | 154 | return null; 155 | } 156 | 157 | /** 158 | * {@inheritDoc} 159 | */ 160 | public function getManagerNames(): array 161 | { 162 | return $this->managers; 163 | } 164 | 165 | /** 166 | * {@inheritDoc} 167 | */ 168 | public function getManagers(): array 169 | { 170 | $managers = []; 171 | 172 | foreach ($this->managers as $name => $id) { 173 | $manager = $this->getService($id); 174 | assert($manager instanceof ObjectManager); 175 | $managers[$name] = $manager; 176 | } 177 | 178 | return $managers; 179 | } 180 | 181 | public function getRepository( 182 | string $persistentObject, 183 | string|null $persistentManagerName = null, 184 | ): ObjectRepository { 185 | return $this 186 | ->selectManager($persistentObject, $persistentManagerName) 187 | ->getRepository($persistentObject); 188 | } 189 | 190 | public function resetManager(string|null $name = null): ObjectManager 191 | { 192 | if ($name === null) { 193 | $name = $this->defaultManager; 194 | } 195 | 196 | if (! isset($this->managers[$name])) { 197 | throw new InvalidArgumentException(sprintf('Doctrine %s Manager named "%s" does not exist.', $this->name, $name)); 198 | } 199 | 200 | // force the creation of a new document manager 201 | // if the current one is closed 202 | $this->resetService($this->managers[$name]); 203 | 204 | return $this->getManager($name); 205 | } 206 | 207 | /** @phpstan-param class-string $persistentObject */ 208 | private function selectManager( 209 | string $persistentObject, 210 | string|null $persistentManagerName = null, 211 | ): ObjectManager { 212 | if ($persistentManagerName !== null) { 213 | return $this->getManager($persistentManagerName); 214 | } 215 | 216 | return $this->getManagerForClass($persistentObject) ?? $this->getManager(); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/Persistence/ConnectionRegistry.php: -------------------------------------------------------------------------------- 1 | An array of Connection instances. 30 | */ 31 | public function getConnections(): array; 32 | 33 | /** 34 | * Gets all connection names. 35 | * 36 | * @return array An array of connection names. 37 | */ 38 | public function getConnectionNames(): array; 39 | } 40 | -------------------------------------------------------------------------------- /src/Persistence/Event/LifecycleEventArgs.php: -------------------------------------------------------------------------------- 1 | object; 29 | } 30 | 31 | /** 32 | * Retrieves the associated ObjectManager. 33 | * 34 | * @phpstan-return TObjectManager 35 | */ 36 | public function getObjectManager(): ObjectManager 37 | { 38 | return $this->objectManager; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Persistence/Event/LoadClassMetadataEventArgs.php: -------------------------------------------------------------------------------- 1 | 15 | * @template-covariant TObjectManager of ObjectManager 16 | */ 17 | class LoadClassMetadataEventArgs extends EventArgs 18 | { 19 | /** 20 | * @phpstan-param TClassMetadata $classMetadata 21 | * @phpstan-param TObjectManager $objectManager 22 | */ 23 | public function __construct( 24 | private readonly ClassMetadata $classMetadata, 25 | private readonly ObjectManager $objectManager, 26 | ) { 27 | } 28 | 29 | /** 30 | * Retrieves the associated ClassMetadata. 31 | * 32 | * @phpstan-return TClassMetadata 33 | */ 34 | public function getClassMetadata(): ClassMetadata 35 | { 36 | return $this->classMetadata; 37 | } 38 | 39 | /** 40 | * Retrieves the associated ObjectManager. 41 | * 42 | * @phpstan-return TObjectManager 43 | */ 44 | public function getObjectManager(): ObjectManager 45 | { 46 | return $this->objectManager; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Persistence/Event/ManagerEventArgs.php: -------------------------------------------------------------------------------- 1 | objectManager; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Persistence/Event/OnClearEventArgs.php: -------------------------------------------------------------------------------- 1 | objectManager; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Persistence/Event/PreUpdateEventArgs.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class PreUpdateEventArgs extends LifecycleEventArgs 19 | { 20 | /** @var array> */ 21 | private array $entityChangeSet; 22 | 23 | /** 24 | * @param array> $changeSet 25 | * @phpstan-param TObjectManager $objectManager 26 | */ 27 | public function __construct(object $entity, ObjectManager $objectManager, array &$changeSet) 28 | { 29 | parent::__construct($entity, $objectManager); 30 | 31 | $this->entityChangeSet = &$changeSet; 32 | } 33 | 34 | /** 35 | * Retrieves the entity changeset. 36 | * 37 | * @return array> 38 | */ 39 | public function getEntityChangeSet(): array 40 | { 41 | return $this->entityChangeSet; 42 | } 43 | 44 | /** Checks if field has a changeset. */ 45 | public function hasChangedField(string $field): bool 46 | { 47 | return isset($this->entityChangeSet[$field]); 48 | } 49 | 50 | /** Gets the old value of the changeset of the changed field. */ 51 | public function getOldValue(string $field): mixed 52 | { 53 | $this->assertValidField($field); 54 | 55 | return $this->entityChangeSet[$field][0]; 56 | } 57 | 58 | /** Gets the new value of the changeset of the changed field. */ 59 | public function getNewValue(string $field): mixed 60 | { 61 | $this->assertValidField($field); 62 | 63 | return $this->entityChangeSet[$field][1]; 64 | } 65 | 66 | /** Sets the new value of this field. */ 67 | public function setNewValue(string $field, mixed $value): void 68 | { 69 | $this->assertValidField($field); 70 | 71 | $this->entityChangeSet[$field][1] = $value; 72 | } 73 | 74 | /** 75 | * Asserts the field exists in changeset. 76 | * 77 | * @throws InvalidArgumentException 78 | */ 79 | private function assertValidField(string $field): void 80 | { 81 | if (! isset($this->entityChangeSet[$field])) { 82 | throw new InvalidArgumentException(sprintf( 83 | 'Field "%s" is not a valid field of the entity "%s" in PreUpdateEventArgs.', 84 | $field, 85 | $this->getObject()::class, 86 | )); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Persistence/ManagerRegistry.php: -------------------------------------------------------------------------------- 1 | An array of ObjectManager instances 30 | */ 31 | public function getManagers(): array; 32 | 33 | /** 34 | * Resets a named object manager. 35 | * 36 | * This method is useful when an object manager has been closed 37 | * because of a rollbacked transaction AND when you think that 38 | * it makes sense to get a new one to replace the closed one. 39 | * 40 | * Be warned that you will get a brand new object manager as 41 | * the existing one is not useable anymore. This means that any 42 | * other object with a dependency on this object manager will 43 | * hold an obsolete reference. You can inject the registry instead 44 | * to avoid this problem. 45 | * 46 | * @param string|null $name The object manager name (null for the default one). 47 | */ 48 | public function resetManager(string|null $name = null): ObjectManager; 49 | 50 | /** 51 | * Gets all object manager names and associated service IDs. A service ID 52 | * is a string that allows to obtain an object manager, typically from a 53 | * PSR-11 container. 54 | * 55 | * @return array An array with object manager names as keys, 56 | * and service IDs as values. 57 | */ 58 | public function getManagerNames(): array; 59 | 60 | /** 61 | * Gets the ObjectRepository for a persistent object. 62 | * 63 | * @param string $persistentObject The name of the persistent object. 64 | * @param string|null $persistentManagerName The object manager name (null for the default one). 65 | * @phpstan-param class-string $persistentObject 66 | * 67 | * @phpstan-return ObjectRepository 68 | * 69 | * @template T of object 70 | */ 71 | public function getRepository( 72 | string $persistentObject, 73 | string|null $persistentManagerName = null, 74 | ): ObjectRepository; 75 | 76 | /** 77 | * Gets the object manager associated with a given class. 78 | * 79 | * @param class-string $class A persistent object class name. 80 | */ 81 | public function getManagerForClass(string $class): ObjectManager|null; 82 | } 83 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/AbstractClassMetadataFactory.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | abstract class AbstractClassMetadataFactory implements ClassMetadataFactory 37 | { 38 | /** Salt used by specific Object Manager implementation. */ 39 | protected string $cacheSalt = '__CLASSMETADATA__'; 40 | 41 | private CacheItemPoolInterface|null $cache = null; 42 | 43 | /** 44 | * @var array 45 | * @phpstan-var CMTemplate[] 46 | */ 47 | private array $loadedMetadata = []; 48 | 49 | protected bool $initialized = false; 50 | 51 | private ReflectionService|null $reflectionService = null; 52 | 53 | private ProxyClassNameResolver|null $proxyClassNameResolver = null; 54 | 55 | public function setCache(CacheItemPoolInterface $cache): void 56 | { 57 | $this->cache = $cache; 58 | } 59 | 60 | final protected function getCache(): CacheItemPoolInterface|null 61 | { 62 | return $this->cache; 63 | } 64 | 65 | /** 66 | * Returns an array of all the loaded metadata currently in memory. 67 | * 68 | * @return ClassMetadata[] 69 | * @phpstan-return CMTemplate[] 70 | */ 71 | public function getLoadedMetadata(): array 72 | { 73 | return $this->loadedMetadata; 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | public function getAllMetadata(): array 80 | { 81 | if (! $this->initialized) { 82 | $this->initialize(); 83 | } 84 | 85 | $driver = $this->getDriver(); 86 | $metadata = []; 87 | foreach ($driver->getAllClassNames() as $className) { 88 | $metadata[] = $this->getMetadataFor($className); 89 | } 90 | 91 | return $metadata; 92 | } 93 | 94 | public function setProxyClassNameResolver(ProxyClassNameResolver $resolver): void 95 | { 96 | $this->proxyClassNameResolver = $resolver; 97 | } 98 | 99 | /** 100 | * Lazy initialization of this stuff, especially the metadata driver, 101 | * since these are not needed at all when a metadata cache is active. 102 | */ 103 | abstract protected function initialize(): void; 104 | 105 | /** Returns the mapping driver implementation. */ 106 | abstract protected function getDriver(): MappingDriver; 107 | 108 | /** 109 | * Wakes up reflection after ClassMetadata gets unserialized from cache. 110 | * 111 | * @phpstan-param CMTemplate $class 112 | */ 113 | abstract protected function wakeupReflection( 114 | ClassMetadata $class, 115 | ReflectionService $reflService, 116 | ): void; 117 | 118 | /** 119 | * Initializes Reflection after ClassMetadata was constructed. 120 | * 121 | * @phpstan-param CMTemplate $class 122 | */ 123 | abstract protected function initializeReflection( 124 | ClassMetadata $class, 125 | ReflectionService $reflService, 126 | ): void; 127 | 128 | /** 129 | * Checks whether the class metadata is an entity. 130 | * 131 | * This method should return false for mapped superclasses or embedded classes. 132 | * 133 | * @phpstan-param CMTemplate $class 134 | */ 135 | abstract protected function isEntity(ClassMetadata $class): bool; 136 | 137 | /** 138 | * Removes the prepended backslash of a class string to conform with how php outputs class names 139 | * 140 | * @phpstan-param class-string $className 141 | * 142 | * @phpstan-return class-string 143 | */ 144 | private function normalizeClassName(string $className): string 145 | { 146 | return ltrim($className, '\\'); 147 | } 148 | 149 | /** 150 | * {@inheritDoc} 151 | * 152 | * @throws ReflectionException 153 | * @throws MappingException 154 | */ 155 | public function getMetadataFor(string $className): ClassMetadata 156 | { 157 | $className = $this->normalizeClassName($className); 158 | 159 | if (isset($this->loadedMetadata[$className])) { 160 | return $this->loadedMetadata[$className]; 161 | } 162 | 163 | if (class_exists($className, false) && (new ReflectionClass($className))->isAnonymous()) { 164 | throw MappingException::classIsAnonymous($className); 165 | } 166 | 167 | if (! class_exists($className, false) && str_contains($className, ':')) { 168 | throw MappingException::nonExistingClass($className); 169 | } 170 | 171 | $realClassName = $this->getRealClass($className); 172 | 173 | if (isset($this->loadedMetadata[$realClassName])) { 174 | // We do not have the alias name in the map, include it 175 | return $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName]; 176 | } 177 | 178 | try { 179 | if ($this->cache !== null) { 180 | $cached = $this->cache->getItem($this->getCacheKey($realClassName))->get(); 181 | if ($cached instanceof ClassMetadata) { 182 | /** @phpstan-var CMTemplate $cached */ 183 | $this->loadedMetadata[$realClassName] = $cached; 184 | 185 | $this->wakeupReflection($cached, $this->getReflectionService()); 186 | } else { 187 | $loadedMetadata = $this->loadMetadata($realClassName); 188 | $classNames = array_combine( 189 | array_map($this->getCacheKey(...), $loadedMetadata), 190 | $loadedMetadata, 191 | ); 192 | 193 | foreach ($this->cache->getItems(array_keys($classNames)) as $item) { 194 | if (! isset($classNames[$item->getKey()])) { 195 | continue; 196 | } 197 | 198 | $item->set($this->loadedMetadata[$classNames[$item->getKey()]]); 199 | $this->cache->saveDeferred($item); 200 | } 201 | 202 | $this->cache->commit(); 203 | } 204 | } else { 205 | $this->loadMetadata($realClassName); 206 | } 207 | } catch (MappingException $loadingException) { 208 | $fallbackMetadataResponse = $this->onNotFoundMetadata($realClassName); 209 | 210 | if ($fallbackMetadataResponse === null) { 211 | throw $loadingException; 212 | } 213 | 214 | $this->loadedMetadata[$realClassName] = $fallbackMetadataResponse; 215 | } 216 | 217 | if ($className !== $realClassName) { 218 | // We do not have the alias name in the map, include it 219 | $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName]; 220 | } 221 | 222 | return $this->loadedMetadata[$className]; 223 | } 224 | 225 | public function hasMetadataFor(string $className): bool 226 | { 227 | $className = $this->normalizeClassName($className); 228 | 229 | return isset($this->loadedMetadata[$className]); 230 | } 231 | 232 | /** 233 | * Sets the metadata descriptor for a specific class. 234 | * 235 | * NOTE: This is only useful in very special cases, like when generating proxy classes. 236 | * 237 | * @phpstan-param class-string $className 238 | * @phpstan-param CMTemplate $class 239 | */ 240 | public function setMetadataFor(string $className, ClassMetadata $class): void 241 | { 242 | $this->loadedMetadata[$this->normalizeClassName($className)] = $class; 243 | } 244 | 245 | /** 246 | * Gets an array of parent classes for the given entity class. 247 | * 248 | * @phpstan-param class-string $name 249 | * 250 | * @return string[] 251 | * @phpstan-return list 252 | */ 253 | protected function getParentClasses(string $name): array 254 | { 255 | // Collect parent classes, ignoring transient (not-mapped) classes. 256 | $parentClasses = []; 257 | 258 | foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) { 259 | if ($this->getDriver()->isTransient($parentClass)) { 260 | continue; 261 | } 262 | 263 | $parentClasses[] = $parentClass; 264 | } 265 | 266 | return $parentClasses; 267 | } 268 | 269 | /** 270 | * Loads the metadata of the class in question and all it's ancestors whose metadata 271 | * is still not loaded. 272 | * 273 | * Important: The class $name does not necessarily exist at this point here. 274 | * Scenarios in a code-generation setup might have access to XML/YAML 275 | * Mapping files without the actual PHP code existing here. That is why the 276 | * {@see \Doctrine\Persistence\Mapping\ReflectionService} interface 277 | * should be used for reflection. 278 | * 279 | * @param string $name The name of the class for which the metadata should get loaded. 280 | * @phpstan-param class-string $name 281 | * 282 | * @return array 283 | * @phpstan-return list 284 | */ 285 | protected function loadMetadata(string $name): array 286 | { 287 | if (! $this->initialized) { 288 | $this->initialize(); 289 | } 290 | 291 | $loaded = []; 292 | 293 | $parentClasses = $this->getParentClasses($name); 294 | $parentClasses[] = $name; 295 | 296 | // Move down the hierarchy of parent classes, starting from the topmost class 297 | $parent = null; 298 | $rootEntityFound = false; 299 | $visited = []; 300 | $reflService = $this->getReflectionService(); 301 | 302 | foreach ($parentClasses as $className) { 303 | if (isset($this->loadedMetadata[$className])) { 304 | $parent = $this->loadedMetadata[$className]; 305 | 306 | if ($this->isEntity($parent)) { 307 | $rootEntityFound = true; 308 | 309 | array_unshift($visited, $className); 310 | } 311 | 312 | continue; 313 | } 314 | 315 | $class = $this->newClassMetadataInstance($className); 316 | $this->initializeReflection($class, $reflService); 317 | 318 | $this->doLoadMetadata($class, $parent, $rootEntityFound, $visited); 319 | 320 | $this->loadedMetadata[$className] = $class; 321 | 322 | $parent = $class; 323 | 324 | if ($this->isEntity($class)) { 325 | $rootEntityFound = true; 326 | 327 | array_unshift($visited, $className); 328 | } 329 | 330 | $this->wakeupReflection($class, $reflService); 331 | 332 | $loaded[] = $className; 333 | } 334 | 335 | return $loaded; 336 | } 337 | 338 | /** 339 | * Provides a fallback hook for loading metadata when loading failed due to reflection/mapping exceptions 340 | * 341 | * Override this method to implement a fallback strategy for failed metadata loading 342 | * 343 | * @phpstan-return CMTemplate|null 344 | */ 345 | protected function onNotFoundMetadata(string $className): ClassMetadata|null 346 | { 347 | return null; 348 | } 349 | 350 | /** 351 | * Actually loads the metadata from the underlying metadata. 352 | * 353 | * @param bool $rootEntityFound True when there is another entity (non-mapped superclass) class above the current class in the PHP class hierarchy. 354 | * @param list $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. 355 | * @phpstan-param CMTemplate $class 356 | * @phpstan-param CMTemplate|null $parent 357 | */ 358 | abstract protected function doLoadMetadata( 359 | ClassMetadata $class, 360 | ClassMetadata|null $parent, 361 | bool $rootEntityFound, 362 | array $nonSuperclassParents, 363 | ): void; 364 | 365 | /** 366 | * Creates a new ClassMetadata instance for the given class name. 367 | * 368 | * @phpstan-param class-string $className 369 | * 370 | * @return ClassMetadata 371 | * @phpstan-return CMTemplate 372 | * 373 | * @template T of object 374 | */ 375 | abstract protected function newClassMetadataInstance(string $className): ClassMetadata; 376 | 377 | public function isTransient(string $className): bool 378 | { 379 | if (! $this->initialized) { 380 | $this->initialize(); 381 | } 382 | 383 | if (class_exists($className, false) && (new ReflectionClass($className))->isAnonymous()) { 384 | return false; 385 | } 386 | 387 | if (! class_exists($className, false) && str_contains($className, ':')) { 388 | throw MappingException::nonExistingClass($className); 389 | } 390 | 391 | /** @phpstan-var class-string $className */ 392 | return $this->getDriver()->isTransient($className); 393 | } 394 | 395 | /** Sets the reflectionService. */ 396 | public function setReflectionService(ReflectionService $reflectionService): void 397 | { 398 | $this->reflectionService = $reflectionService; 399 | } 400 | 401 | /** Gets the reflection service associated with this metadata factory. */ 402 | public function getReflectionService(): ReflectionService 403 | { 404 | if ($this->reflectionService === null) { 405 | $this->reflectionService = new RuntimeReflectionService(); 406 | } 407 | 408 | return $this->reflectionService; 409 | } 410 | 411 | protected function getCacheKey(string $realClassName): string 412 | { 413 | return str_replace('\\', '__', $realClassName) . $this->cacheSalt; 414 | } 415 | 416 | /** 417 | * Gets the real class name of a class name that could be a proxy. 418 | * 419 | * @phpstan-param class-string>|class-string $class 420 | * 421 | * @phpstan-return class-string 422 | * 423 | * @template T of object 424 | */ 425 | private function getRealClass(string $class): string 426 | { 427 | if ($this->proxyClassNameResolver === null) { 428 | $this->createDefaultProxyClassNameResolver(); 429 | } 430 | 431 | assert($this->proxyClassNameResolver !== null); 432 | 433 | return $this->proxyClassNameResolver->resolveClassName($class); 434 | } 435 | 436 | private function createDefaultProxyClassNameResolver(): void 437 | { 438 | $this->proxyClassNameResolver = new class implements ProxyClassNameResolver { 439 | /** 440 | * @phpstan-param class-string>|class-string $className 441 | * 442 | * @phpstan-return class-string 443 | * 444 | * @template T of object 445 | */ 446 | public function resolveClassName(string $className): string 447 | { 448 | $pos = strrpos($className, '\\' . Proxy::MARKER . '\\'); 449 | 450 | if ($pos === false) { 451 | /** @phpstan-var class-string */ 452 | return $className; 453 | } 454 | 455 | /** @phpstan-var class-string */ 456 | return substr($className, $pos + Proxy::MARKER_LENGTH + 2); 457 | } 458 | }; 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/ClassMetadata.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function getName(): string; 22 | 23 | /** 24 | * Gets the mapped identifier field name. 25 | * 26 | * The returned structure is an array of the identifier field names. 27 | * 28 | * @return array 29 | * @phpstan-return list 30 | */ 31 | public function getIdentifier(): array; 32 | 33 | /** 34 | * Gets the ReflectionClass instance for this mapped class. 35 | * 36 | * @return ReflectionClass 37 | */ 38 | public function getReflectionClass(): ReflectionClass; 39 | 40 | /** Checks if the given field name is a mapped identifier for this class. */ 41 | public function isIdentifier(string $fieldName): bool; 42 | 43 | /** Checks if the given field is a mapped property for this class. */ 44 | public function hasField(string $fieldName): bool; 45 | 46 | /** Checks if the given field is a mapped association for this class. */ 47 | public function hasAssociation(string $fieldName): bool; 48 | 49 | /** Checks if the given field is a mapped single valued association for this class. */ 50 | public function isSingleValuedAssociation(string $fieldName): bool; 51 | 52 | /** Checks if the given field is a mapped collection valued association for this class. */ 53 | public function isCollectionValuedAssociation(string $fieldName): bool; 54 | 55 | /** 56 | * A numerically indexed list of field names of this persistent class. 57 | * 58 | * This array includes identifier fields if present on this class. 59 | * 60 | * @return array 61 | */ 62 | public function getFieldNames(): array; 63 | 64 | /** 65 | * Returns an array of identifier field names numerically indexed. 66 | * 67 | * @return array 68 | */ 69 | public function getIdentifierFieldNames(): array; 70 | 71 | /** 72 | * Returns a numerically indexed list of association names of this persistent class. 73 | * 74 | * This array includes identifier associations if present on this class. 75 | * 76 | * @return array 77 | */ 78 | public function getAssociationNames(): array; 79 | 80 | /** 81 | * Returns a type name of this field. 82 | * 83 | * This type names can be implementation specific but should at least include the php types: 84 | * integer, string, boolean, float/double, datetime. 85 | */ 86 | public function getTypeOfField(string $fieldName): string|null; 87 | 88 | /** 89 | * Returns the target class name of the given association. 90 | * 91 | * @phpstan-return class-string|null 92 | */ 93 | public function getAssociationTargetClass(string $assocName): string|null; 94 | 95 | /** Checks if the association is the inverse side of a bidirectional association. */ 96 | public function isAssociationInverseSide(string $assocName): bool; 97 | 98 | /** Returns the target field of the owning side of the association. */ 99 | public function getAssociationMappedByTargetField(string $assocName): string; 100 | 101 | /** 102 | * Returns the identifier of this object as an array with field name as key. 103 | * 104 | * Has to return an empty array if no identifier isset. 105 | * 106 | * @return array 107 | */ 108 | public function getIdentifierValues(object $object): array; 109 | } 110 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/ClassMetadataFactory.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function getAllMetadata(): array; 22 | 23 | /** 24 | * Gets the class metadata descriptor for a class. 25 | * 26 | * @param class-string $className The name of the class. 27 | * 28 | * @phpstan-return T 29 | */ 30 | public function getMetadataFor(string $className): ClassMetadata; 31 | 32 | /** 33 | * Checks whether the factory has the metadata for a class loaded already. 34 | * 35 | * @param class-string $className 36 | * 37 | * @return bool TRUE if the metadata of the class in question is already loaded, FALSE otherwise. 38 | */ 39 | public function hasMetadataFor(string $className): bool; 40 | 41 | /** 42 | * Sets the metadata descriptor for a specific class. 43 | * 44 | * @param class-string $className 45 | * @phpstan-param T $class 46 | */ 47 | public function setMetadataFor(string $className, ClassMetadata $class): void; 48 | 49 | /** 50 | * Returns whether the class with the specified name should have its metadata loaded. 51 | * This is only the case if it is either mapped directly or as a MappedSuperclass. 52 | * 53 | * @phpstan-param class-string $className 54 | */ 55 | public function isTransient(string $className): bool; 56 | } 57 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/Driver/ColocatedMappingDriver.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | protected array $paths = []; 38 | 39 | /** 40 | * The paths excluded from path where to look for mapping files. 41 | * 42 | * @var array 43 | */ 44 | protected array $excludePaths = []; 45 | 46 | /** The file extension of mapping documents. */ 47 | protected string $fileExtension = '.php'; 48 | 49 | /** 50 | * Cache for getAllClassNames(). 51 | * 52 | * @var array|null 53 | * @phpstan-var list|null 54 | */ 55 | protected array|null $classNames = null; 56 | 57 | /** 58 | * Appends lookup paths to metadata driver. 59 | * 60 | * @param array $paths 61 | */ 62 | public function addPaths(array $paths): void 63 | { 64 | $this->paths = array_unique(array_merge($this->paths, $paths)); 65 | } 66 | 67 | /** 68 | * Retrieves the defined metadata lookup paths. 69 | * 70 | * @return array 71 | */ 72 | public function getPaths(): array 73 | { 74 | return $this->paths; 75 | } 76 | 77 | /** 78 | * Append exclude lookup paths to metadata driver. 79 | * 80 | * @param string[] $paths 81 | */ 82 | public function addExcludePaths(array $paths): void 83 | { 84 | $this->excludePaths = array_unique(array_merge($this->excludePaths, $paths)); 85 | } 86 | 87 | /** 88 | * Retrieve the defined metadata lookup exclude paths. 89 | * 90 | * @return array 91 | */ 92 | public function getExcludePaths(): array 93 | { 94 | return $this->excludePaths; 95 | } 96 | 97 | /** Gets the file extension used to look for mapping files under. */ 98 | public function getFileExtension(): string 99 | { 100 | return $this->fileExtension; 101 | } 102 | 103 | /** Sets the file extension used to look for mapping files under. */ 104 | public function setFileExtension(string $fileExtension): void 105 | { 106 | $this->fileExtension = $fileExtension; 107 | } 108 | 109 | /** 110 | * {@inheritDoc} 111 | * 112 | * Returns whether the class with the specified name is transient. Only non-transient 113 | * classes, that is entities and mapped superclasses, should have their metadata loaded. 114 | * 115 | * @phpstan-param class-string $className 116 | */ 117 | abstract public function isTransient(string $className): bool; 118 | 119 | /** 120 | * Gets the names of all mapped classes known to this driver. 121 | * 122 | * @return string[] The names of all mapped classes known to this driver. 123 | * @phpstan-return list 124 | */ 125 | public function getAllClassNames(): array 126 | { 127 | if ($this->classNames !== null) { 128 | return $this->classNames; 129 | } 130 | 131 | if ($this->paths === []) { 132 | throw MappingException::pathRequiredForDriver(static::class); 133 | } 134 | 135 | $classes = []; 136 | $includedFiles = []; 137 | 138 | foreach ($this->paths as $path) { 139 | if (! is_dir($path)) { 140 | throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); 141 | } 142 | 143 | $iterator = new RegexIterator( 144 | new RecursiveIteratorIterator( 145 | new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), 146 | RecursiveIteratorIterator::LEAVES_ONLY, 147 | ), 148 | '/^.+' . preg_quote($this->fileExtension) . '$/i', 149 | RecursiveRegexIterator::GET_MATCH, 150 | ); 151 | 152 | foreach ($iterator as $file) { 153 | $sourceFile = $file[0]; 154 | 155 | if (preg_match('(^phar:)i', $sourceFile) === 0) { 156 | $sourceFile = realpath($sourceFile); 157 | } 158 | 159 | foreach ($this->excludePaths as $excludePath) { 160 | $realExcludePath = realpath($excludePath); 161 | assert($realExcludePath !== false); 162 | $exclude = str_replace('\\', '/', $realExcludePath); 163 | $current = str_replace('\\', '/', $sourceFile); 164 | 165 | if (str_contains($current, $exclude)) { 166 | continue 2; 167 | } 168 | } 169 | 170 | require_once $sourceFile; 171 | 172 | $includedFiles[] = $sourceFile; 173 | } 174 | } 175 | 176 | $declared = get_declared_classes(); 177 | 178 | foreach ($declared as $className) { 179 | $rc = new ReflectionClass($className); 180 | 181 | $sourceFile = $rc->getFileName(); 182 | 183 | if (! in_array($sourceFile, $includedFiles, true) || $this->isTransient($className)) { 184 | continue; 185 | } 186 | 187 | $classes[] = $className; 188 | } 189 | 190 | $this->classNames = $classes; 191 | 192 | return $classes; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/Driver/DefaultFileLocator.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | protected array $paths = []; 34 | 35 | /** The file extension of mapping documents. */ 36 | protected string|null $fileExtension; 37 | 38 | /** 39 | * Initializes a new FileDriver that looks in the given path(s) for mapping 40 | * documents and operates in the specified operating mode. 41 | * 42 | * @param string|array $paths One or multiple paths where mapping documents 43 | * can be found. 44 | * @param string|null $fileExtension The file extension of mapping documents, 45 | * usually prefixed with a dot. 46 | */ 47 | public function __construct(string|array $paths, string|null $fileExtension = null) 48 | { 49 | $this->addPaths((array) $paths); 50 | $this->fileExtension = $fileExtension; 51 | } 52 | 53 | /** 54 | * Appends lookup paths to metadata driver. 55 | * 56 | * @param array $paths 57 | */ 58 | public function addPaths(array $paths): void 59 | { 60 | $this->paths = array_unique([...$this->paths, ...$paths]); 61 | } 62 | 63 | /** 64 | * Retrieves the defined metadata lookup paths. 65 | * 66 | * @return array 67 | */ 68 | public function getPaths(): array 69 | { 70 | return $this->paths; 71 | } 72 | 73 | /** Gets the file extension used to look for mapping files under. */ 74 | public function getFileExtension(): string|null 75 | { 76 | return $this->fileExtension; 77 | } 78 | 79 | /** 80 | * Sets the file extension used to look for mapping files under. 81 | * 82 | * @param string|null $fileExtension The file extension to set. 83 | */ 84 | public function setFileExtension(string|null $fileExtension): void 85 | { 86 | $this->fileExtension = $fileExtension; 87 | } 88 | 89 | public function findMappingFile(string $className): string 90 | { 91 | $fileName = str_replace('\\', '.', $className) . $this->fileExtension; 92 | 93 | // Check whether file exists 94 | foreach ($this->paths as $path) { 95 | if (is_file($path . DIRECTORY_SEPARATOR . $fileName)) { 96 | return $path . DIRECTORY_SEPARATOR . $fileName; 97 | } 98 | } 99 | 100 | throw MappingException::mappingFileNotFound($className, $fileName); 101 | } 102 | 103 | /** 104 | * {@inheritDoc} 105 | */ 106 | public function getAllClassNames(string $globalBasename): array 107 | { 108 | if ($this->paths === []) { 109 | return []; 110 | } 111 | 112 | $classes = []; 113 | 114 | foreach ($this->paths as $path) { 115 | if (! is_dir($path)) { 116 | throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); 117 | } 118 | 119 | $iterator = new RecursiveIteratorIterator( 120 | new RecursiveDirectoryIterator($path), 121 | RecursiveIteratorIterator::LEAVES_ONLY, 122 | ); 123 | 124 | foreach ($iterator as $file) { 125 | $fileName = $file->getBasename($this->fileExtension); 126 | 127 | if ($fileName === $file->getBasename() || $fileName === $globalBasename) { 128 | continue; 129 | } 130 | 131 | // NOTE: All files found here means classes are not transient! 132 | 133 | assert(is_string($fileName)); 134 | /** @phpstan-var class-string */ 135 | $class = str_replace('.', '\\', $fileName); 136 | $classes[] = $class; 137 | } 138 | } 139 | 140 | return $classes; 141 | } 142 | 143 | public function fileExists(string $className): bool 144 | { 145 | $fileName = str_replace('\\', '.', $className) . $this->fileExtension; 146 | 147 | // Check whether file exists 148 | foreach ($this->paths as $path) { 149 | if (is_file($path . DIRECTORY_SEPARATOR . $fileName)) { 150 | return true; 151 | } 152 | } 153 | 154 | return false; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/Driver/FileDriver.php: -------------------------------------------------------------------------------- 1 | |null 33 | */ 34 | protected array|null $classCache = null; 35 | protected string $globalBasename = ''; 36 | 37 | /** 38 | * Initializes a new FileDriver that looks in the given path(s) for mapping 39 | * documents and operates in the specified operating mode. 40 | * 41 | * @param string|array|FileLocator $locator A FileLocator or one/multiple paths 42 | * where mapping documents can be found. 43 | */ 44 | public function __construct(string|array|FileLocator $locator, string|null $fileExtension = null) 45 | { 46 | if ($locator instanceof FileLocator) { 47 | $this->locator = $locator; 48 | } else { 49 | $this->locator = new DefaultFileLocator((array) $locator, $fileExtension); 50 | } 51 | } 52 | 53 | /** Sets the global basename. */ 54 | public function setGlobalBasename(string $file): void 55 | { 56 | $this->globalBasename = $file; 57 | } 58 | 59 | /** Retrieves the global basename. */ 60 | public function getGlobalBasename(): string 61 | { 62 | return $this->globalBasename; 63 | } 64 | 65 | /** 66 | * Gets the element of schema meta data for the class from the mapping file. 67 | * This will lazily load the mapping file if it is not loaded yet. 68 | * 69 | * @phpstan-param class-string $className 70 | * 71 | * @return T The element of schema meta data. 72 | * 73 | * @throws MappingException 74 | */ 75 | public function getElement(string $className): mixed 76 | { 77 | if ($this->classCache === null) { 78 | $this->initialize(); 79 | } 80 | 81 | if (isset($this->classCache[$className])) { 82 | return $this->classCache[$className]; 83 | } 84 | 85 | $result = $this->loadMappingFile($this->locator->findMappingFile($className)); 86 | 87 | if (! isset($result[$className])) { 88 | throw MappingException::invalidMappingFile( 89 | $className, 90 | str_replace('\\', '.', $className) . $this->locator->getFileExtension(), 91 | ); 92 | } 93 | 94 | $this->classCache[$className] = $result[$className]; 95 | 96 | return $result[$className]; 97 | } 98 | 99 | public function isTransient(string $className): bool 100 | { 101 | if ($this->classCache === null) { 102 | $this->initialize(); 103 | } 104 | 105 | if (isset($this->classCache[$className])) { 106 | return false; 107 | } 108 | 109 | return ! $this->locator->fileExists($className); 110 | } 111 | 112 | /** 113 | * {@inheritDoc} 114 | */ 115 | public function getAllClassNames(): array 116 | { 117 | if ($this->classCache === null) { 118 | $this->initialize(); 119 | } 120 | 121 | if ($this->classCache === []) { 122 | return $this->locator->getAllClassNames($this->globalBasename); 123 | } 124 | 125 | /** @phpstan-var array> $classCache */ 126 | $classCache = $this->classCache; 127 | 128 | /** @var list $keys */ 129 | $keys = array_keys($classCache); 130 | 131 | return array_values(array_unique([...$keys, ...$this->locator->getAllClassNames($this->globalBasename)])); 132 | } 133 | 134 | /** 135 | * Loads a mapping file with the given name and returns a map 136 | * from class/entity names to their corresponding file driver elements. 137 | * 138 | * @param string $file The mapping file to load. 139 | * 140 | * @return mixed[] 141 | * @phpstan-return array 142 | */ 143 | abstract protected function loadMappingFile(string $file): array; 144 | 145 | /** 146 | * Initializes the class cache from all the global files. 147 | * 148 | * Using this feature adds a substantial performance hit to file drivers as 149 | * more metadata has to be loaded into memory than might actually be 150 | * necessary. This may not be relevant to scenarios where caching of 151 | * metadata is in place, however hits very hard in scenarios where no 152 | * caching is used. 153 | */ 154 | protected function initialize(): void 155 | { 156 | $this->classCache = []; 157 | if ($this->globalBasename === '') { 158 | return; 159 | } 160 | 161 | foreach ($this->locator->getPaths() as $path) { 162 | $file = $path . '/' . $this->globalBasename . $this->locator->getFileExtension(); 163 | if (! is_file($file)) { 164 | continue; 165 | } 166 | 167 | $this->classCache = [...$this->classCache, ...$this->loadMappingFile($file)]; 168 | } 169 | } 170 | 171 | /** Retrieves the locator used to discover mapping files by className. */ 172 | public function getLocator(): FileLocator 173 | { 174 | return $this->locator; 175 | } 176 | 177 | /** Sets the locator used to discover mapping files by className. */ 178 | public function setLocator(FileLocator $locator): void 179 | { 180 | $this->locator = $locator; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/Driver/FileLocator.php: -------------------------------------------------------------------------------- 1 | 24 | * @phpstan-return list 25 | */ 26 | public function getAllClassNames(string $globalBasename): array; 27 | 28 | /** Checks if a file can be found for this class name. */ 29 | public function fileExists(string $className): bool; 30 | 31 | /** 32 | * Gets all the paths that this file locator looks for mapping files. 33 | * 34 | * @return array 35 | */ 36 | public function getPaths(): array; 37 | 38 | /** Gets the file extension that mapping files are suffixed with. */ 39 | public function getFileExtension(): string|null; 40 | } 41 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/Driver/MappingDriver.php: -------------------------------------------------------------------------------- 1 | $className 18 | * @phpstan-param ClassMetadata $metadata 19 | * 20 | * @template T of object 21 | */ 22 | public function loadMetadataForClass(string $className, ClassMetadata $metadata): void; 23 | 24 | /** 25 | * Gets the names of all mapped classes known to this driver. 26 | * 27 | * @return array The names of all mapped classes known to this driver. 28 | * @phpstan-return list 29 | */ 30 | public function getAllClassNames(): array; 31 | 32 | /** 33 | * Returns whether the class with the specified name should have its metadata loaded. 34 | * This is only the case if it is either mapped as an Entity or a MappedSuperclass. 35 | * 36 | * @phpstan-param class-string $className 37 | */ 38 | public function isTransient(string $className): bool; 39 | } 40 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/Driver/MappingDriverChain.php: -------------------------------------------------------------------------------- 1 | */ 26 | private array $drivers = []; 27 | 28 | /** Gets the default driver. */ 29 | public function getDefaultDriver(): MappingDriver|null 30 | { 31 | return $this->defaultDriver; 32 | } 33 | 34 | /** Set the default driver. */ 35 | public function setDefaultDriver(MappingDriver $driver): void 36 | { 37 | $this->defaultDriver = $driver; 38 | } 39 | 40 | /** Adds a nested driver. */ 41 | public function addDriver(MappingDriver $nestedDriver, string $namespace): void 42 | { 43 | $this->drivers[$namespace] = $nestedDriver; 44 | } 45 | 46 | /** 47 | * Gets the array of nested drivers. 48 | * 49 | * @return array $drivers 50 | */ 51 | public function getDrivers(): array 52 | { 53 | return $this->drivers; 54 | } 55 | 56 | public function loadMetadataForClass(string $className, ClassMetadata $metadata): void 57 | { 58 | foreach ($this->drivers as $namespace => $driver) { 59 | if (str_starts_with($className, $namespace)) { 60 | $driver->loadMetadataForClass($className, $metadata); 61 | 62 | return; 63 | } 64 | } 65 | 66 | if ($this->defaultDriver !== null) { 67 | $this->defaultDriver->loadMetadataForClass($className, $metadata); 68 | 69 | return; 70 | } 71 | 72 | throw MappingException::classNotFoundInNamespaces($className, array_keys($this->drivers)); 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function getAllClassNames(): array 79 | { 80 | $classNames = []; 81 | $driverClasses = []; 82 | 83 | foreach ($this->drivers as $namespace => $driver) { 84 | $oid = spl_object_hash($driver); 85 | 86 | if (! isset($driverClasses[$oid])) { 87 | $driverClasses[$oid] = $driver->getAllClassNames(); 88 | } 89 | 90 | foreach ($driverClasses[$oid] as $className) { 91 | if (! str_starts_with($className, $namespace)) { 92 | continue; 93 | } 94 | 95 | $classNames[$className] = true; 96 | } 97 | } 98 | 99 | if ($this->defaultDriver !== null) { 100 | foreach ($this->defaultDriver->getAllClassNames() as $className) { 101 | $classNames[$className] = true; 102 | } 103 | } 104 | 105 | return array_keys($classNames); 106 | } 107 | 108 | public function isTransient(string $className): bool 109 | { 110 | foreach ($this->drivers as $namespace => $driver) { 111 | if (str_starts_with($className, $namespace)) { 112 | return $driver->isTransient($className); 113 | } 114 | } 115 | 116 | if ($this->defaultDriver !== null) { 117 | return $this->defaultDriver->isTransient($className); 118 | } 119 | 120 | return true; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/Driver/PHPDriver.php: -------------------------------------------------------------------------------- 1 | > 14 | */ 15 | class PHPDriver extends FileDriver 16 | { 17 | /** @phpstan-var ClassMetadata */ 18 | protected ClassMetadata $metadata; 19 | 20 | /** @param string|array|FileLocator $locator */ 21 | public function __construct(string|array|FileLocator $locator) 22 | { 23 | parent::__construct($locator, '.php'); 24 | } 25 | 26 | public function loadMetadataForClass(string $className, ClassMetadata $metadata): void 27 | { 28 | $this->metadata = $metadata; 29 | 30 | $this->loadMappingFile($this->locator->findMappingFile($className)); 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | protected function loadMappingFile(string $file): array 37 | { 38 | $metadata = $this->metadata; 39 | include $file; 40 | 41 | return [$metadata->getName() => $metadata]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/Driver/StaticPHPDriver.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | private array $paths = []; 32 | 33 | /** 34 | * Map of all class names. 35 | * 36 | * @var array 37 | * @phpstan-var list 38 | */ 39 | private array|null $classNames = null; 40 | 41 | /** @param array|string $paths */ 42 | public function __construct(array|string $paths) 43 | { 44 | $this->addPaths((array) $paths); 45 | } 46 | 47 | /** @param array $paths */ 48 | public function addPaths(array $paths): void 49 | { 50 | $this->paths = array_unique([...$this->paths, ...$paths]); 51 | } 52 | 53 | public function loadMetadataForClass(string $className, ClassMetadata $metadata): void 54 | { 55 | $className::loadMetadata($metadata); 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | * 61 | * @todo Same code exists in ColocatedMappingDriver, should we re-use it 62 | * somehow or not worry about it? 63 | */ 64 | public function getAllClassNames(): array 65 | { 66 | if ($this->classNames !== null) { 67 | return $this->classNames; 68 | } 69 | 70 | if ($this->paths === []) { 71 | throw MappingException::pathRequiredForDriver(static::class); 72 | } 73 | 74 | $classes = []; 75 | $includedFiles = []; 76 | 77 | foreach ($this->paths as $path) { 78 | if (! is_dir($path)) { 79 | throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); 80 | } 81 | 82 | $iterator = new RecursiveIteratorIterator( 83 | new RecursiveDirectoryIterator($path), 84 | RecursiveIteratorIterator::LEAVES_ONLY, 85 | ); 86 | 87 | foreach ($iterator as $file) { 88 | if ($file->getBasename('.php') === $file->getBasename()) { 89 | continue; 90 | } 91 | 92 | $sourceFile = realpath($file->getPathName()); 93 | require_once $sourceFile; 94 | $includedFiles[] = $sourceFile; 95 | } 96 | } 97 | 98 | $declared = get_declared_classes(); 99 | 100 | foreach ($declared as $className) { 101 | $rc = new ReflectionClass($className); 102 | 103 | $sourceFile = $rc->getFileName(); 104 | 105 | if (! in_array($sourceFile, $includedFiles, true) || $this->isTransient($className)) { 106 | continue; 107 | } 108 | 109 | $classes[] = $className; 110 | } 111 | 112 | $this->classNames = $classes; 113 | 114 | return $classes; 115 | } 116 | 117 | public function isTransient(string $className): bool 118 | { 119 | return ! method_exists($className, 'loadMetadata'); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/Driver/SymfonyFileLocator.php: -------------------------------------------------------------------------------- 1 | 40 | */ 41 | protected array $paths = []; 42 | 43 | /** 44 | * A map of mapping directory path to namespace prefix used to expand class shortnames. 45 | * 46 | * @var array 47 | */ 48 | protected array $prefixes = []; 49 | 50 | /** File extension that is searched for. */ 51 | protected string|null $fileExtension; 52 | 53 | /** 54 | * Represents PHP namespace delimiters when looking for files 55 | */ 56 | private readonly string $nsSeparator; 57 | 58 | /** 59 | * @param array $prefixes 60 | * @param string $nsSeparator String which would be used when converting FQCN 61 | * to filename and vice versa. Should not be empty 62 | */ 63 | public function __construct( 64 | array $prefixes, 65 | string $fileExtension = '', 66 | string $nsSeparator = '.', 67 | ) { 68 | $this->addNamespacePrefixes($prefixes); 69 | $this->fileExtension = $fileExtension; 70 | 71 | if ($nsSeparator === '') { 72 | throw new InvalidArgumentException('Namespace separator should not be empty'); 73 | } 74 | 75 | $this->nsSeparator = $nsSeparator; 76 | } 77 | 78 | /** 79 | * Adds Namespace Prefixes. 80 | * 81 | * @param array $prefixes 82 | */ 83 | public function addNamespacePrefixes(array $prefixes): void 84 | { 85 | $this->prefixes = [...$this->prefixes, ...$prefixes]; 86 | $this->paths = [...$this->paths, ...array_keys($prefixes)]; 87 | } 88 | 89 | /** 90 | * Gets Namespace Prefixes. 91 | * 92 | * @return string[] 93 | */ 94 | public function getNamespacePrefixes(): array 95 | { 96 | return $this->prefixes; 97 | } 98 | 99 | /** 100 | * {@inheritDoc} 101 | */ 102 | public function getPaths(): array 103 | { 104 | return $this->paths; 105 | } 106 | 107 | public function getFileExtension(): string|null 108 | { 109 | return $this->fileExtension; 110 | } 111 | 112 | /** 113 | * Sets the file extension used to look for mapping files under. 114 | * 115 | * @param string $fileExtension The file extension to set. 116 | */ 117 | public function setFileExtension(string $fileExtension): void 118 | { 119 | $this->fileExtension = $fileExtension; 120 | } 121 | 122 | public function fileExists(string $className): bool 123 | { 124 | $defaultFileName = str_replace('\\', $this->nsSeparator, $className) . $this->fileExtension; 125 | foreach ($this->paths as $path) { 126 | if (! isset($this->prefixes[$path])) { 127 | // global namespace class 128 | if (is_file($path . DIRECTORY_SEPARATOR . $defaultFileName)) { 129 | return true; 130 | } 131 | 132 | continue; 133 | } 134 | 135 | $prefix = $this->prefixes[$path]; 136 | 137 | if (! str_starts_with($className, $prefix . '\\')) { 138 | continue; 139 | } 140 | 141 | $filename = $path . '/' . strtr(substr($className, strlen($prefix) + 1), '\\', $this->nsSeparator) . $this->fileExtension; 142 | 143 | if (is_file($filename)) { 144 | return true; 145 | } 146 | } 147 | 148 | return false; 149 | } 150 | 151 | /** 152 | * {@inheritDoc} 153 | */ 154 | public function getAllClassNames(string|null $globalBasename = null): array 155 | { 156 | if ($this->paths === []) { 157 | return []; 158 | } 159 | 160 | $classes = []; 161 | 162 | foreach ($this->paths as $path) { 163 | if (! is_dir($path)) { 164 | throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); 165 | } 166 | 167 | $iterator = new RecursiveIteratorIterator( 168 | new RecursiveDirectoryIterator($path), 169 | RecursiveIteratorIterator::LEAVES_ONLY, 170 | ); 171 | 172 | foreach ($iterator as $file) { 173 | $fileName = $file->getBasename($this->fileExtension); 174 | 175 | if ($fileName === $file->getBasename() || $fileName === $globalBasename) { 176 | continue; 177 | } 178 | 179 | // NOTE: All files found here means classes are not transient! 180 | if (isset($this->prefixes[$path])) { 181 | // Calculate namespace suffix for given prefix as a relative path from basepath to file path 182 | $nsSuffix = strtr( 183 | substr($this->realpath($file->getPath()), strlen($this->realpath($path))), 184 | $this->nsSeparator, 185 | '\\', 186 | ); 187 | 188 | /** @phpstan-var class-string */ 189 | $class = $this->prefixes[$path] . str_replace(DIRECTORY_SEPARATOR, '\\', $nsSuffix) . '\\' . str_replace($this->nsSeparator, '\\', $fileName); 190 | } else { 191 | /** @phpstan-var class-string */ 192 | $class = str_replace($this->nsSeparator, '\\', $fileName); 193 | } 194 | 195 | $classes[] = $class; 196 | } 197 | } 198 | 199 | return $classes; 200 | } 201 | 202 | public function findMappingFile(string $className): string 203 | { 204 | $defaultFileName = str_replace('\\', $this->nsSeparator, $className) . $this->fileExtension; 205 | foreach ($this->paths as $path) { 206 | if (! isset($this->prefixes[$path])) { 207 | if (is_file($path . DIRECTORY_SEPARATOR . $defaultFileName)) { 208 | return $path . DIRECTORY_SEPARATOR . $defaultFileName; 209 | } 210 | 211 | continue; 212 | } 213 | 214 | $prefix = $this->prefixes[$path]; 215 | 216 | if (! str_starts_with($className, $prefix . '\\')) { 217 | continue; 218 | } 219 | 220 | $filename = $path . '/' . strtr(substr($className, strlen($prefix) + 1), '\\', $this->nsSeparator) . $this->fileExtension; 221 | if (is_file($filename)) { 222 | return $filename; 223 | } 224 | } 225 | 226 | $pos = strrpos($className, '\\'); 227 | assert(is_int($pos)); 228 | 229 | throw MappingException::mappingFileNotFound( 230 | $className, 231 | substr($className, $pos + 1) . $this->fileExtension, 232 | ); 233 | } 234 | 235 | private function realpath(string $path): string 236 | { 237 | $realpath = realpath($path); 238 | 239 | if ($realpath === false) { 240 | throw new RuntimeException(sprintf('Could not get realpath for %s', $path)); 241 | } 242 | 243 | return $realpath; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/MappingException.php: -------------------------------------------------------------------------------- 1 | $namespaces */ 18 | public static function classNotFoundInNamespaces( 19 | string $className, 20 | array $namespaces, 21 | ): self { 22 | return new self(sprintf( 23 | "The class '%s' was not found in the chain configured namespaces %s", 24 | $className, 25 | implode(', ', $namespaces), 26 | )); 27 | } 28 | 29 | /** @param class-string $driverClassName */ 30 | public static function pathRequiredForDriver(string $driverClassName): self 31 | { 32 | return new self(sprintf( 33 | 'Specifying the paths to your entities is required when using %s to retrieve all class names.', 34 | $driverClassName, 35 | )); 36 | } 37 | 38 | public static function fileMappingDriversRequireConfiguredDirectoryPath( 39 | string|null $path = null, 40 | ): self { 41 | if ($path !== null) { 42 | $path = '[' . $path . ']'; 43 | } 44 | 45 | return new self(sprintf( 46 | 'File mapping drivers must have a valid directory path, ' . 47 | 'however the given path %s seems to be incorrect!', 48 | (string) $path, 49 | )); 50 | } 51 | 52 | public static function mappingFileNotFound(string $entityName, string $fileName): self 53 | { 54 | return new self(sprintf( 55 | "No mapping file found named '%s' for class '%s'.", 56 | $fileName, 57 | $entityName, 58 | )); 59 | } 60 | 61 | public static function invalidMappingFile(string $entityName, string $fileName): self 62 | { 63 | return new self(sprintf( 64 | "Invalid mapping file '%s' for class '%s'.", 65 | $fileName, 66 | $entityName, 67 | )); 68 | } 69 | 70 | public static function nonExistingClass(string $className): self 71 | { 72 | return new self(sprintf("Class '%s' does not exist", $className)); 73 | } 74 | 75 | /** @param class-string $className */ 76 | public static function classIsAnonymous(string $className): self 77 | { 78 | return new self(sprintf('Class "%s" is anonymous', $className)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/ProxyClassNameResolver.php: -------------------------------------------------------------------------------- 1 | >|class-string $className 13 | * 14 | * @phpstan-return class-string 15 | * 16 | * @template T of object 17 | */ 18 | public function resolveClassName(string $className): string; 19 | } 20 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/ReflectionService.php: -------------------------------------------------------------------------------- 1 | $class 44 | * 45 | * @phpstan-return ReflectionClass 46 | * 47 | * @template T of object 48 | */ 49 | public function getClass(string $class): ReflectionClass; 50 | 51 | /** 52 | * Returns an accessible property (setAccessible(true)) or null. 53 | * 54 | * @phpstan-param class-string $class 55 | */ 56 | public function getAccessibleProperty(string $class, string $property): ReflectionProperty|null; 57 | 58 | /** 59 | * Checks if the class have a public method with the given name. 60 | * 61 | * @phpstan-param class-string $class 62 | */ 63 | public function hasPublicMethod(string $class, string $method): bool; 64 | } 65 | -------------------------------------------------------------------------------- /src/Persistence/Mapping/RuntimeReflectionService.php: -------------------------------------------------------------------------------- 1 | supportsTypedPropertiesWorkaround = version_compare(phpversion(), '7.4.0') >= 0; 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function getParentClasses(string $class): array 36 | { 37 | if (! class_exists($class)) { 38 | throw MappingException::nonExistingClass($class); 39 | } 40 | 41 | $parents = class_parents($class); 42 | 43 | assert($parents !== false); 44 | 45 | return $parents; 46 | } 47 | 48 | public function getClassShortName(string $class): string 49 | { 50 | $reflectionClass = new ReflectionClass($class); 51 | 52 | return $reflectionClass->getShortName(); 53 | } 54 | 55 | public function getClassNamespace(string $class): string 56 | { 57 | $reflectionClass = new ReflectionClass($class); 58 | 59 | return $reflectionClass->getNamespaceName(); 60 | } 61 | 62 | /** 63 | * @phpstan-param class-string $class 64 | * 65 | * @phpstan-return ReflectionClass 66 | * 67 | * @template T of object 68 | */ 69 | public function getClass(string $class): ReflectionClass 70 | { 71 | return new ReflectionClass($class); 72 | } 73 | 74 | public function getAccessibleProperty(string $class, string $property): RuntimeReflectionProperty 75 | { 76 | $reflectionProperty = new RuntimeReflectionProperty($class, $property); 77 | 78 | if ($this->supportsTypedPropertiesWorkaround && ! array_key_exists($property, $this->getClass($class)->getDefaultProperties())) { 79 | $reflectionProperty = new TypedNoDefaultReflectionProperty($class, $property); 80 | } 81 | 82 | $reflectionProperty->setAccessible(true); 83 | 84 | return $reflectionProperty; 85 | } 86 | 87 | public function hasPublicMethod(string $class, string $method): bool 88 | { 89 | try { 90 | $reflectionMethod = new ReflectionMethod($class, $method); 91 | } catch (ReflectionException) { 92 | return false; 93 | } 94 | 95 | return $reflectionMethod->isPublic(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Persistence/NotifyPropertyChanged.php: -------------------------------------------------------------------------------- 1 | find($id). 17 | * 18 | * @param string $className The class name of the object to find. 19 | * @param mixed $id The identity of the object to find. 20 | * @phpstan-param class-string $className 21 | * 22 | * @return object|null The found object. 23 | * @phpstan-return T|null 24 | * 25 | * @template T of object 26 | */ 27 | public function find(string $className, mixed $id): object|null; 28 | 29 | /** 30 | * Tells the ObjectManager to make an instance managed and persistent. 31 | * 32 | * The object will be entered into the database as a result of the flush operation. 33 | * 34 | * NOTE: The persist operation always considers objects that are not yet known to 35 | * this ObjectManager as NEW. Do not pass detached objects to the persist operation. 36 | * 37 | * @param object $object The instance to make managed and persistent. 38 | */ 39 | public function persist(object $object): void; 40 | 41 | /** 42 | * Removes an object instance. 43 | * 44 | * A removed object will be removed from the database as a result of the flush operation. 45 | * 46 | * @param object $object The object instance to remove. 47 | */ 48 | public function remove(object $object): void; 49 | 50 | /** 51 | * Clears the ObjectManager. All objects that are currently managed 52 | * by this ObjectManager become detached. 53 | */ 54 | public function clear(): void; 55 | 56 | /** 57 | * Detaches an object from the ObjectManager, causing a managed object to 58 | * become detached. Unflushed changes made to the object if any 59 | * (including removal of the object), will not be synchronized to the database. 60 | * Objects which previously referenced the detached object will continue to 61 | * reference it. 62 | * 63 | * @param object $object The object to detach. 64 | */ 65 | public function detach(object $object): void; 66 | 67 | /** 68 | * Refreshes the persistent state of an object from the database, 69 | * overriding any local changes that have not yet been persisted. 70 | * 71 | * @param object $object The object to refresh. 72 | */ 73 | public function refresh(object $object): void; 74 | 75 | /** 76 | * Flushes all changes to objects that have been queued up to now to the database. 77 | * This effectively synchronizes the in-memory state of managed objects with the 78 | * database. 79 | */ 80 | public function flush(): void; 81 | 82 | /** 83 | * Gets the repository for a class. 84 | * 85 | * @phpstan-param class-string $className 86 | * 87 | * @phpstan-return ObjectRepository 88 | * 89 | * @template T of object 90 | */ 91 | public function getRepository(string $className): ObjectRepository; 92 | 93 | /** 94 | * Returns the ClassMetadata descriptor for a class. 95 | * 96 | * The class name must be the fully-qualified class name without a leading backslash 97 | * (as it is returned by get_class($obj)). 98 | * 99 | * @phpstan-param class-string $className 100 | * 101 | * @phpstan-return ClassMetadata 102 | * 103 | * @template T of object 104 | */ 105 | public function getClassMetadata(string $className): ClassMetadata; 106 | 107 | /** 108 | * Gets the metadata factory used to gather the metadata of classes. 109 | * 110 | * @phpstan-return ClassMetadataFactory> 111 | */ 112 | public function getMetadataFactory(): ClassMetadataFactory; 113 | 114 | /** 115 | * Helper method to initialize a lazy loading proxy or persistent collection. 116 | * 117 | * This method is a no-op for other objects. 118 | */ 119 | public function initializeObject(object $obj): void; 120 | 121 | /** Helper method to check whether a lazy loading proxy or persistent collection has been initialized. */ 122 | public function isUninitializedObject(mixed $value): bool; 123 | 124 | /** 125 | * Checks if the object is part of the current UnitOfWork and therefore managed. 126 | */ 127 | public function contains(object $object): bool; 128 | } 129 | -------------------------------------------------------------------------------- /src/Persistence/ObjectManagerDecorator.php: -------------------------------------------------------------------------------- 1 | wrapped->find($className, $id); 26 | } 27 | 28 | public function persist(object $object): void 29 | { 30 | $this->wrapped->persist($object); 31 | } 32 | 33 | public function remove(object $object): void 34 | { 35 | $this->wrapped->remove($object); 36 | } 37 | 38 | public function clear(): void 39 | { 40 | $this->wrapped->clear(); 41 | } 42 | 43 | public function detach(object $object): void 44 | { 45 | $this->wrapped->detach($object); 46 | } 47 | 48 | public function refresh(object $object): void 49 | { 50 | $this->wrapped->refresh($object); 51 | } 52 | 53 | public function flush(): void 54 | { 55 | $this->wrapped->flush(); 56 | } 57 | 58 | public function getRepository(string $className): ObjectRepository 59 | { 60 | return $this->wrapped->getRepository($className); 61 | } 62 | 63 | public function getClassMetadata(string $className): ClassMetadata 64 | { 65 | return $this->wrapped->getClassMetadata($className); 66 | } 67 | 68 | /** @phpstan-return ClassMetadataFactory> */ 69 | public function getMetadataFactory(): ClassMetadataFactory 70 | { 71 | return $this->wrapped->getMetadataFactory(); 72 | } 73 | 74 | public function initializeObject(object $obj): void 75 | { 76 | $this->wrapped->initializeObject($obj); 77 | } 78 | 79 | public function isUninitializedObject(mixed $value): bool 80 | { 81 | return $this->wrapped->isUninitializedObject($value); 82 | } 83 | 84 | public function contains(object $object): bool 85 | { 86 | return $this->wrapped->contains($object); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Persistence/ObjectRepository.php: -------------------------------------------------------------------------------- 1 | The objects. 30 | * @phpstan-return T[] 31 | */ 32 | public function findAll(): array; 33 | 34 | /** 35 | * Finds objects by a set of criteria. 36 | * 37 | * Optionally sorting and limiting details can be passed. An implementation may throw 38 | * an UnexpectedValueException if certain values of the sorting or limiting details are 39 | * not supported. 40 | * 41 | * @param array $criteria 42 | * @param array|null $orderBy 43 | * @phpstan-param array|null $orderBy 44 | * 45 | * @return array The objects. 46 | * @phpstan-return T[] 47 | * 48 | * @throws UnexpectedValueException 49 | */ 50 | public function findBy( 51 | array $criteria, 52 | array|null $orderBy = null, 53 | int|null $limit = null, 54 | int|null $offset = null, 55 | ): array; 56 | 57 | /** 58 | * Finds a single object by a set of criteria. 59 | * 60 | * @param array $criteria The criteria. 61 | * 62 | * @return object|null The object. 63 | * @phpstan-return T|null 64 | */ 65 | public function findOneBy(array $criteria): object|null; 66 | 67 | /** 68 | * Returns the class name of the object managed by the repository. 69 | * 70 | * @phpstan-return class-string 71 | */ 72 | public function getClassName(): string; 73 | } 74 | -------------------------------------------------------------------------------- /src/Persistence/PropertyChangedListener.php: -------------------------------------------------------------------------------- 1 | $enumType */ 22 | public function __construct(private readonly ReflectionProperty $originalReflectionProperty, private readonly string $enumType) 23 | { 24 | } 25 | 26 | public function getDeclaringClass(): ReflectionClass 27 | { 28 | return $this->originalReflectionProperty->getDeclaringClass(); 29 | } 30 | 31 | public function getName(): string 32 | { 33 | return $this->originalReflectionProperty->getName(); 34 | } 35 | 36 | public function getType(): ReflectionType|null 37 | { 38 | return $this->originalReflectionProperty->getType(); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function getAttributes(string|null $name = null, int $flags = 0): array 45 | { 46 | return $this->originalReflectionProperty->getAttributes($name, $flags); 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | * 52 | * Converts enum instance to its value. 53 | * 54 | * @param object|null $object 55 | * 56 | * @return int|string|int[]|string[]|null 57 | */ 58 | public function getValue($object = null): int|string|array|null 59 | { 60 | if ($object === null) { 61 | return null; 62 | } 63 | 64 | $enum = $this->originalReflectionProperty->getValue($object); 65 | 66 | if ($enum === null) { 67 | return null; 68 | } 69 | 70 | return $this->fromEnum($enum); 71 | } 72 | 73 | /** 74 | * Converts enum value to enum instance. 75 | * 76 | * @param object|null $object 77 | */ 78 | public function setValue(mixed $object, mixed $value = null): void 79 | { 80 | if ($value !== null) { 81 | $value = $this->toEnum($value); 82 | } 83 | 84 | $this->originalReflectionProperty->setValue($object, $value); 85 | } 86 | 87 | /** 88 | * @param BackedEnum|BackedEnum[] $enum 89 | * 90 | * @return ($enum is BackedEnum ? (string|int) : (string[]|int[])) 91 | */ 92 | private function fromEnum(BackedEnum|array $enum) 93 | { 94 | if (is_array($enum)) { 95 | return array_map(static fn (BackedEnum $enum) => $enum->value, $enum); 96 | } 97 | 98 | return $enum->value; 99 | } 100 | 101 | /** 102 | * @param int|string|int[]|string[]|BackedEnum|BackedEnum[] $value 103 | * 104 | * @return ($value is int|string|BackedEnum ? BackedEnum : BackedEnum[]) 105 | */ 106 | private function toEnum(int|string|array|BackedEnum $value) 107 | { 108 | if ($value instanceof BackedEnum) { 109 | return $value; 110 | } 111 | 112 | if (is_array($value)) { 113 | $v = reset($value); 114 | if ($v instanceof BackedEnum) { 115 | return $value; 116 | } 117 | 118 | return array_map([$this->enumType, 'from'], $value); 119 | } 120 | 121 | return $this->enumType::from($value); 122 | } 123 | 124 | public function getModifiers(): int 125 | { 126 | return $this->originalReflectionProperty->getModifiers(); 127 | } 128 | 129 | public function getDocComment(): string|false 130 | { 131 | return $this->originalReflectionProperty->getDocComment(); 132 | } 133 | 134 | public function isPrivate(): bool 135 | { 136 | return $this->originalReflectionProperty->isPrivate(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Persistence/Reflection/RuntimeReflectionProperty.php: -------------------------------------------------------------------------------- 1 | key = $this->isPrivate() ? "\0" . ltrim($class, '\\') . "\0" . $name : ($this->isProtected() ? "\0*\0" . $name : $name); 29 | } 30 | 31 | public function getValue(object|null $object = null): mixed 32 | { 33 | if ($object === null) { 34 | return parent::getValue($object); 35 | } 36 | 37 | return ((array) $object)[$this->key] ?? null; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | * 43 | * @param object|null $object 44 | */ 45 | public function setValue(mixed $object, mixed $value = null): void 46 | { 47 | if (! ($object instanceof Proxy && ! $object->__isInitialized())) { 48 | parent::setValue($object, $value); 49 | 50 | return; 51 | } 52 | 53 | if (! method_exists($object, '__setInitialized')) { 54 | return; 55 | } 56 | 57 | $object->__setInitialized(true); 58 | 59 | parent::setValue($object, $value); 60 | 61 | $object->__setInitialized(false); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Persistence/Reflection/TypedNoDefaultReflectionProperty.php: -------------------------------------------------------------------------------- 1 | isInitialized($object) ? parent::getValue($object) : null; 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | * 31 | * Works around the problem with setting typed no default properties to 32 | * NULL which is not supported, instead unset() to uninitialize. 33 | * 34 | * @link https://github.com/doctrine/orm/issues/7999 35 | * 36 | * @param object|null $object 37 | */ 38 | public function setValue(mixed $object, mixed $value = null): void 39 | { 40 | if ($value === null && $this->hasType() && ! $this->getType()->allowsNull()) { 41 | $propertyName = $this->getName(); 42 | 43 | $unsetter = function () use ($propertyName): void { 44 | unset($this->$propertyName); 45 | }; 46 | $unsetter = $unsetter->bindTo($object, $this->getDeclaringClass()->getName()); 47 | 48 | assert($unsetter instanceof Closure); 49 | 50 | $unsetter(); 51 | 52 | return; 53 | } 54 | 55 | parent::setValue($object, $value); 56 | } 57 | } 58 | --------------------------------------------------------------------------------