├── .github ├── .kodiak.toml └── workflows │ ├── codesniffer.yml │ ├── coverage.yml │ ├── phpstan.yml │ └── tests.yml ├── .phpstorm.meta.php ├── LICENSE ├── composer.json └── src ├── DI ├── Helpers │ ├── BuilderMan.php │ ├── MappingHelper.php │ └── SmartStatement.php ├── OrmExtension.php └── Pass │ ├── AbstractPass.php │ ├── ConsolePass.php │ ├── DoctrinePass.php │ ├── EventPass.php │ └── ManagerPass.php ├── Events └── ContainerEventManager.php ├── Exception ├── LogicalException.php └── RuntimeException.php ├── ManagerProvider.php ├── ManagerRegistry.php ├── Mapping └── ContainerEntityListenerResolver.php └── Utils └── Binder.php /.github/.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [merge] 4 | automerge_label = "automerge" 5 | blacklist_title_regex = "^WIP.*" 6 | blacklist_labels = ["WIP"] 7 | method = "rebase" 8 | delete_branch_on_merge = true 9 | notify_on_conflict = true 10 | optimistic_updates = false 11 | -------------------------------------------------------------------------------- /.github/workflows/codesniffer.yml: -------------------------------------------------------------------------------- 1 | name: "Codesniffer" 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | push: 8 | branches: ["*"] 9 | 10 | schedule: 11 | - cron: "0 8 * * 1" 12 | 13 | jobs: 14 | codesniffer: 15 | name: "Codesniffer" 16 | uses: contributte/.github/.github/workflows/codesniffer.yml@master 17 | with: 18 | php: "8.3" 19 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: "Coverage" 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | push: 8 | branches: ["*"] 9 | 10 | schedule: 11 | - cron: "0 8 * * 1" 12 | 13 | jobs: 14 | coverage: 15 | name: "Nette Tester" 16 | uses: contributte/.github/.github/workflows/nette-tester-coverage-v2.yml@master 17 | with: 18 | php: "8.3" 19 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: "Phpstan" 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | push: 8 | branches: ["*"] 9 | 10 | schedule: 11 | - cron: "0 8 * * 1" 12 | 13 | jobs: 14 | phpstan: 15 | name: "Phpstan" 16 | uses: contributte/.github/.github/workflows/phpstan.yml@master 17 | with: 18 | php: "8.3" 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: "Nette Tester" 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | push: 8 | branches: [ "*" ] 9 | 10 | schedule: 11 | - cron: "0 8 * * 1" 12 | 13 | jobs: 14 | test84: 15 | name: "Nette Tester" 16 | uses: contributte/.github/.github/workflows/nette-tester.yml@master 17 | with: 18 | php: "8.4" 19 | 20 | test83: 21 | name: "Nette Tester" 22 | uses: contributte/.github/.github/workflows/nette-tester.yml@master 23 | with: 24 | php: "8.3" 25 | 26 | test82: 27 | name: "Nette Tester" 28 | uses: contributte/.github/.github/workflows/nette-tester.yml@master 29 | with: 30 | php: "8.2" 31 | 32 | testlower: 33 | name: "Nette Tester" 34 | uses: contributte/.github/.github/workflows/nette-tester.yml@master 35 | with: 36 | php: "8.2" 37 | composer: "composer update --no-interaction --no-progress --prefer-dist --prefer-stable --prefer-lowest" 38 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | '@', 7 | ])); 8 | override(\Doctrine\ORM\EntityManagerInterface::getReference(0), map([ 9 | '' => '@', 10 | ])); 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nettrine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nettrine/orm", 3 | "description": "Doctrine ORM for Nette Framework", 4 | "keywords": ["nette", "doctrine", "orm", "database"], 5 | "type": "library", 6 | "license": "MIT", 7 | "homepage": "https://github.com/contributte/doctrine-orm", 8 | "authors": [ 9 | { 10 | "name": "Milan Felix Šulc", 11 | "homepage": "https://f3l1x.io" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=8.2", 16 | "doctrine/orm": "^3.3.0", 17 | "nette/di": "^3.1.2", 18 | "nettrine/dbal": "^0.10.2 || ^0.11.0", 19 | "psr/cache": "^3.0.0", 20 | "psr/log": "^3.0.2" 21 | }, 22 | "require-dev": { 23 | "contributte/phpstan": "^0.2.0", 24 | "contributte/qa": "^0.4", 25 | "contributte/tester": "^0.3.0", 26 | "symfony/console": "^7.1.8 ", 27 | "symfony/cache": "^7.1.9", 28 | "monolog/monolog": "^3.8.0", 29 | "mockery/mockery": "^1.6.12", 30 | "tracy/tracy": "^2.10.3" 31 | }, 32 | "conflict": { 33 | "doctrine/event-manager": "<2.0.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Nettrine\\ORM\\": "src/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Tests\\": "tests" 43 | } 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true, 47 | "config": { 48 | "sort-packages": true, 49 | "allow-plugins": { 50 | "dealerdirect/phpcodesniffer-composer-installer": true 51 | } 52 | }, 53 | "extra": { 54 | "branch-alias": { 55 | "dev-master": "0.10.x-dev" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/DI/Helpers/BuilderMan.php: -------------------------------------------------------------------------------- 1 | pass = $pass; 20 | } 21 | 22 | public static function of(AbstractPass $pass): self 23 | { 24 | return new self($pass); 25 | } 26 | 27 | public function getConnectionByName(string $connectionName): ServiceDefinition 28 | { 29 | $connections = $this->getConnections(); 30 | 31 | if (!isset($connections[$connectionName])) { 32 | throw new LogicalException(sprintf('Connection "%s" not found', $connectionName)); 33 | } 34 | 35 | return $connections[$connectionName]; 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | public function getConnections(): array 42 | { 43 | $builder = $this->pass->getContainerBuilder(); 44 | $definitions = []; 45 | 46 | /** @var array{name: string} $tagValue */ 47 | foreach ($builder->findByTag(DbalExtension::CONNECTION_TAG) as $serviceName => $tagValue) { 48 | $serviceDef = $builder->getDefinition($serviceName); 49 | assert($serviceDef instanceof ServiceDefinition); 50 | 51 | $definitions[$tagValue['name']] = $serviceDef; 52 | } 53 | 54 | return $definitions; 55 | } 56 | 57 | /** 58 | * @return array 59 | */ 60 | public function getConnectionsMap(): array 61 | { 62 | $builder = $this->pass->getContainerBuilder(); 63 | $definitions = []; 64 | 65 | /** @var array{name: string} $tagValue */ 66 | foreach ($builder->findByTag(DbalExtension::CONNECTION_TAG) as $serviceName => $tagValue) { 67 | $definitions[$tagValue['name']] = $serviceName; 68 | } 69 | 70 | return $definitions; 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | public function getManagersMap(): array 77 | { 78 | $builder = $this->pass->getContainerBuilder(); 79 | $definitions = []; 80 | 81 | /** @var array{name: string} $tagValue */ 82 | foreach ($builder->findByTag(OrmExtension::MANAGER_TAG) as $serviceName => $tagValue) { 83 | $definitions[$tagValue['name']] = $serviceName; 84 | } 85 | 86 | /** @var array{name: string} $tagValue */ 87 | foreach ($builder->findByTag(OrmExtension::MANAGER_DECORATOR_TAG) as $serviceName => $tagValue) { 88 | $definitions[$tagValue['name']] = $serviceName; 89 | } 90 | 91 | return $definitions; 92 | } 93 | 94 | /** 95 | * @return array 96 | */ 97 | public function getServiceDefinitionsByTag(string $tag): array 98 | { 99 | $builder = $this->pass->getContainerBuilder(); 100 | $definitions = []; 101 | 102 | foreach ($builder->findByTag($tag) as $serviceName => $tagValue) { 103 | $definitions[(string) $tagValue] = $builder->getDefinition($serviceName); 104 | } 105 | 106 | return $definitions; 107 | } 108 | 109 | /** 110 | * @return array 111 | */ 112 | public function getServiceNamesByTag(string $tag): array 113 | { 114 | $builder = $this->pass->getContainerBuilder(); 115 | $definitions = []; 116 | 117 | foreach ($builder->findByTag($tag) as $serviceName => $tagValue) { 118 | $definitions[(string) $tagValue] = $serviceName; 119 | } 120 | 121 | return $definitions; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/DI/Helpers/MappingHelper.php: -------------------------------------------------------------------------------- 1 | extension = $extension; 21 | } 22 | 23 | public static function of(CompilerExtension $extension): self 24 | { 25 | return new self($extension); 26 | } 27 | 28 | public function addAttribute(string $connection, string $namespace, string $path): self 29 | { 30 | if (!is_dir($path)) { 31 | throw new LogicalException(sprintf('Given mapping path "%s" does not exist', $path)); 32 | } 33 | 34 | $chainDriver = $this->getChainDriver($connection); 35 | $chainDriver->addSetup('addDriver', [ 36 | new Statement(AttributeDriver::class, [[$path]]), 37 | $namespace, 38 | ]); 39 | 40 | return $this; 41 | } 42 | 43 | public function addXml(string $connection, string $namespace, string $path): self 44 | { 45 | if (!is_dir($path)) { 46 | throw new LogicalException(sprintf('Given mapping path "%s" does not exist', $path)); 47 | } 48 | 49 | $chainDriver = $this->getChainDriver($connection); 50 | $chainDriver->addSetup('addDriver', [ 51 | new Statement(SimplifiedXmlDriver::class, [[$path => $namespace]]), 52 | $namespace, 53 | ]); 54 | 55 | return $this; 56 | } 57 | 58 | private function getChainDriver(string $connection): ServiceDefinition 59 | { 60 | $builder = $this->extension->getContainerBuilder(); 61 | 62 | /** @var array $services */ 63 | $services = $builder->findByTag(OrmExtension::MAPPING_DRIVER_TAG); 64 | 65 | if ($services === []) { 66 | throw new LogicalException('No mapping driver found'); 67 | } 68 | 69 | foreach ($services as $serviceName => $tagValue) { 70 | if ($tagValue['name'] === $connection) { 71 | $serviceDef = $builder->getDefinition($serviceName); 72 | assert($serviceDef instanceof ServiceDefinition); 73 | 74 | return $serviceDef; 75 | } 76 | } 77 | 78 | throw new LogicalException(sprintf('No mapping driver found for connection "%s"', $connection)); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/DI/Helpers/SmartStatement.php: -------------------------------------------------------------------------------- 1 | , 31 | * resolveTargetEntities: array, 32 | * customStringFunctions: array, 33 | * customNumericFunctions: array, 34 | * customDatetimeFunctions: array, 35 | * customHydrationModes: array, 36 | * classMetadataFactoryName: string, 37 | * defaultRepositoryClassName: string, 38 | * namingStrategy: string|Statement|null, 39 | * quoteStrategy: string|Statement|null, 40 | * entityListenerResolver: string|Statement|null, 41 | * repositoryFactory: string|Statement|null, 42 | * defaultQueryHints: array, 43 | * filters: array, 44 | * mapping: array, 45 | * defaultCache: string|Statement|null, 46 | * queryCache: string|Statement|null, 47 | * resultCache: string|Statement|null, 48 | * hydrationCache: string|Statement|null, 49 | * metadataCache: string|Statement|null, 50 | * connection: string, 51 | * secondLevelCache: object{ 52 | * enabled: bool, 53 | * cache: string|Statement|null, 54 | * logger: string|Statement|null, 55 | * regions: array 56 | * } 57 | * } 58 | */ 59 | final class OrmExtension extends CompilerExtension 60 | { 61 | 62 | public const MAPPING_DRIVER_TAG = 'nettrine.orm.mapping_driver'; 63 | public const MANAGER_TAG = 'nettrine.orm.manager'; 64 | public const MANAGER_DECORATOR_TAG = 'nettrine.orm.manager_decorator'; 65 | public const CONFIGURATION_TAG = 'nettrine.orm.configuration'; 66 | 67 | /** @var AbstractPass[] */ 68 | protected array $passes = []; 69 | 70 | public function __construct( 71 | private ?bool $debugMode = null 72 | ) 73 | { 74 | if ($this->debugMode === null) { 75 | $this->debugMode = class_exists(Debugger::class) && Debugger::$productionMode === false; 76 | } 77 | 78 | $this->passes[] = new DoctrinePass($this); 79 | $this->passes[] = new ConsolePass($this); 80 | $this->passes[] = new EventPass($this); 81 | $this->passes[] = new ManagerPass($this, $this->debugMode); 82 | } 83 | 84 | public function getConfigSchema(): Schema 85 | { 86 | $parameters = $this->getContainerBuilder()->parameters; 87 | $proxyDir = isset($parameters['tempDir']) ? $parameters['tempDir'] . '/cache/doctrine/orm/proxies' : null; 88 | $autoGenerateProxy = boolval($parameters['debugMode'] ?? true); 89 | 90 | $expectService = Expect::anyOf( 91 | Expect::string()->required()->assert(fn ($input) => str_starts_with($input, '@') || class_exists($input) || interface_exists($input)), 92 | Expect::type(Statement::class)->required(), 93 | ); 94 | 95 | return Expect::structure([ 96 | 'managers' => Expect::arrayOf( 97 | Expect::structure([ 98 | 'connection' => Expect::string()->required(), 99 | 'entityManagerDecoratorClass' => Expect::string()->assert(fn ($input) => is_a($input, EntityManagerDecorator::class, true), 'EntityManager decorator class must be subclass of ' . EntityManagerDecorator::class), 100 | 'configurationClass' => Expect::string(Configuration::class)->assert(fn ($input) => is_a($input, Configuration::class, true), 'Configuration class must be subclass of ' . Configuration::class), 101 | 'proxyDir' => Expect::string()->default($proxyDir)->before(fn (mixed $v) => $v ?? $proxyDir)->assert(fn (mixed $v) => !($v === null || $v === ''), 'proxyDir must be filled'), 102 | 'autoGenerateProxyClasses' => Expect::anyOf(Expect::int(), Expect::bool(), Expect::type(Statement::class))->default($autoGenerateProxy), 103 | 'proxyNamespace' => Expect::string('Nettrine\Proxy')->nullable(), 104 | 'metadataDriverImpl' => Expect::string(), 105 | 'entityNamespaces' => Expect::array(), 106 | 'resolveTargetEntities' => Expect::array(), 107 | 'customStringFunctions' => Expect::array(), 108 | 'customNumericFunctions' => Expect::array(), 109 | 'customDatetimeFunctions' => Expect::array(), 110 | 'customHydrationModes' => Expect::array(), 111 | 'classMetadataFactoryName' => Expect::string(), 112 | 'defaultRepositoryClassName' => Expect::string(), 113 | 'namingStrategy' => (clone $expectService)->default(UnderscoreNamingStrategy::class), 114 | 'quoteStrategy' => (clone $expectService), 115 | 'entityListenerResolver' => (clone $expectService), 116 | 'repositoryFactory' => (clone $expectService), 117 | 'defaultQueryHints' => Expect::array(), 118 | 'filters' => Expect::arrayOf( 119 | Expect::structure([ 120 | 'class' => Expect::string()->required(), 121 | 'enabled' => Expect::bool(false), 122 | ]) 123 | ), 124 | 'mapping' => Expect::arrayOf( 125 | Expect::structure([ 126 | 'type' => Expect::anyOf('attributes', 'xml')->default('attributes'), 127 | 'directories' => Expect::listOf(Expect::string())->min(1)->required(), 128 | 'namespace' => Expect::string()->required(), 129 | ]), 130 | Expect::string() 131 | )->required()->assert(fn ($input) => count($input) > 0, 'At least one mapping must be defined'), 132 | 'defaultCache' => (clone $expectService), 133 | 'queryCache' => (clone $expectService), 134 | 'resultCache' => (clone $expectService), 135 | 'hydrationCache' => (clone $expectService), 136 | 'metadataCache' => (clone $expectService), 137 | 'secondLevelCache' => Expect::structure([ 138 | 'enabled' => Expect::bool()->default(false), 139 | 'cache' => (clone $expectService), 140 | 'logger' => (clone $expectService), 141 | 'regions' => Expect::arrayOf( 142 | Expect::structure([ 143 | 'lifetime' => Expect::int()->required(), 144 | 'lockLifetime' => Expect::int()->required(), 145 | ]), 146 | Expect::string()->required() 147 | ), 148 | ]), 149 | ])->required(), 150 | Expect::string()->required() 151 | ), 152 | ]); 153 | } 154 | 155 | /** 156 | * Register services 157 | */ 158 | public function loadConfiguration(): void 159 | { 160 | // Trigger passes 161 | foreach ($this->passes as $pass) { 162 | $pass->loadPassConfiguration(); 163 | } 164 | } 165 | 166 | /** 167 | * Decorate services 168 | */ 169 | public function beforeCompile(): void 170 | { 171 | // Trigger passes 172 | foreach ($this->passes as $pass) { 173 | $pass->beforePassCompile(); 174 | } 175 | } 176 | 177 | public function afterCompile(ClassType $class): void 178 | { 179 | // Trigger passes 180 | foreach ($this->passes as $pass) { 181 | $pass->afterPassCompile($class); 182 | } 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/DI/Pass/AbstractPass.php: -------------------------------------------------------------------------------- 1 | extension = $extension; 18 | } 19 | 20 | /** 21 | * Register services 22 | */ 23 | public function loadPassConfiguration(): void 24 | { 25 | // Override in child 26 | } 27 | 28 | /** 29 | * Decorate services 30 | */ 31 | public function beforePassCompile(): void 32 | { 33 | // Override in child 34 | } 35 | 36 | /** 37 | * Update PHP code 38 | */ 39 | public function afterPassCompile(ClassType $class): void 40 | { 41 | // Override in child 42 | } 43 | 44 | public function prefix(string $id): string 45 | { 46 | return $this->extension->prefix($id); 47 | } 48 | 49 | public function getContainerBuilder(): ContainerBuilder 50 | { 51 | return $this->extension->getContainerBuilder(); 52 | } 53 | 54 | public function getConfig(): stdclass 55 | { 56 | /** @var stdclass $ret */ 57 | $ret = (object) $this->extension->getConfig(); 58 | 59 | return $ret; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/DI/Pass/ConsolePass.php: -------------------------------------------------------------------------------- 1 | getContainerBuilder(); 21 | 22 | $builder->addDefinition($this->prefix('schemaToolCreateCommand')) 23 | ->setType(CreateCommand::class) 24 | ->addTag('console.command', 'orm:schema-tool:create') 25 | ->setAutowired(false); 26 | 27 | $builder->addDefinition($this->prefix('schemaToolUpdateCommand')) 28 | ->setType(UpdateCommand::class) 29 | ->addTag('console.command', 'orm:schema-tool:update') 30 | ->setAutowired(false); 31 | 32 | $builder->addDefinition($this->prefix('schemaToolDropCommand')) 33 | ->setType(DropCommand::class) 34 | ->addTag('console.command', 'orm:schema-tool:drop') 35 | ->setAutowired(false); 36 | 37 | $builder->addDefinition($this->prefix('generateProxiesCommand')) 38 | ->setType(GenerateProxiesCommand::class) 39 | ->addTag('console.command', 'orm:generate-proxies') 40 | ->setAutowired(false); 41 | 42 | $builder->addDefinition($this->prefix('infoCommand')) 43 | ->setType(InfoCommand::class) 44 | ->addTag('console.command', 'orm:info') 45 | ->setAutowired(false); 46 | 47 | $builder->addDefinition($this->prefix('mappingDescribeCommand')) 48 | ->setType(MappingDescribeCommand::class) 49 | ->addTag('console.command', 'orm:mapping:describe') 50 | ->setAutowired(false); 51 | 52 | $builder->addDefinition($this->prefix('runDqlCommand')) 53 | ->setType(RunDqlCommand::class) 54 | ->addTag('console.command', 'orm:run-dql') 55 | ->setAutowired(false); 56 | 57 | $builder->addDefinition($this->prefix('validateSchemaCommand')) 58 | ->setType(ValidateSchemaCommand::class) 59 | ->addTag('console.command', 'orm:validate-schema') 60 | ->setAutowired(false); 61 | 62 | $builder->addDefinition($this->prefix('clearMetadataCacheCommand')) 63 | ->setType(MetadataCommand::class) 64 | ->addTag('console.command', 'orm:clear-cache:metadata') 65 | ->setAutowired(false); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/DI/Pass/DoctrinePass.php: -------------------------------------------------------------------------------- 1 | extension->getContainerBuilder(); 17 | 18 | // Manager Registry 19 | $builder->addDefinition($this->prefix('managerRegistry')) 20 | ->setFactory(ManagerRegistry::class, [ 21 | '@container', 22 | [], 23 | [], 24 | ]); 25 | 26 | // Manager Provider 27 | $builder->addDefinition($this->prefix('managerProvider')) 28 | ->setFactory(ManagerProvider::class, [ 29 | $this->prefix('@managerRegistry'), 30 | ]); 31 | 32 | // Entity Listener Resolver 33 | $builder->addDefinition($this->prefix('entityListenerResolver')) 34 | ->setType(ContainerEntityListenerResolver::class); 35 | } 36 | 37 | public function beforePassCompile(): void 38 | { 39 | $builder = $this->extension->getContainerBuilder(); 40 | 41 | $managerRegistryDef = $builder->getDefinition($this->prefix('managerRegistry')); 42 | assert($managerRegistryDef instanceof ServiceDefinition); 43 | 44 | $managerRegistryDef->setArgument(1, BuilderMan::of($this)->getConnectionsMap()); 45 | $managerRegistryDef->setArgument(2, BuilderMan::of($this)->getManagersMap()); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/DI/Pass/EventPass.php: -------------------------------------------------------------------------------- 1 | getContainerBuilder(); 16 | 17 | // Event manager 18 | $builder->addDefinition($this->prefix('eventManager')) 19 | ->setFactory(ContainerEventManager::class); 20 | } 21 | 22 | public function beforePassCompile(): void 23 | { 24 | $builder = $this->getContainerBuilder(); 25 | 26 | $eventManagerDef = $builder->getDefinition($this->prefix('eventManager')); 27 | assert($eventManagerDef instanceof ServiceDefinition); 28 | 29 | foreach ($builder->findByType(EventSubscriber::class) as $serviceName => $serviceDef) { 30 | /** @var class-string $serviceClass */ 31 | $serviceClass = (string) $serviceDef->getType(); 32 | $rc = new ReflectionClass($serviceClass); 33 | 34 | /** @var EventSubscriber $subscriber */ 35 | $subscriber = $rc->newInstanceWithoutConstructor(); 36 | $events = $subscriber->getSubscribedEvents(); 37 | 38 | $eventManagerDef->addSetup('?->addEventListener(?, ?)', ['@self', $events, $serviceName]); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/DI/Pass/ManagerPass.php: -------------------------------------------------------------------------------- 1 | getConfig(); 39 | 40 | // Configure managers 41 | foreach ($config->managers as $managerName => $managerConfig) { 42 | // Load connection configuration 43 | $this->loadManagerConfiguration($managerName, $managerConfig); 44 | } 45 | } 46 | 47 | public function beforePassCompile(): void 48 | { 49 | $config = $this->getConfig(); 50 | 51 | // Configure managers 52 | foreach ($config->managers as $managerName => $managerConfig) { 53 | $this->beforeManagerCompile($managerName, $managerConfig); 54 | } 55 | } 56 | 57 | /** 58 | * @phpstan-param TManagerConfig $managerConfig 59 | */ 60 | public function loadManagerConfiguration(string $managerName, mixed $managerConfig): void 61 | { 62 | $builder = $this->getContainerBuilder(); 63 | 64 | // Configuration 65 | $configuration = $builder->addDefinition($this->prefix(sprintf('managers.%s.configuration', $managerName))) 66 | ->setType($managerConfig->configurationClass) 67 | ->addTag(OrmExtension::CONFIGURATION_TAG, ['name' => $managerName]) 68 | ->setAutowired(false); 69 | 70 | // Configuration: proxy dir 71 | if ($managerConfig->proxyDir !== null) { 72 | $configuration->addSetup('setProxyDir', [Helpers::expand($managerConfig->proxyDir, $builder->parameters)]); 73 | } 74 | 75 | // Configuration: auto generate proxy classes 76 | if (is_bool($managerConfig->autoGenerateProxyClasses)) { 77 | $defaultStrategy = $this->debugMode === true ? ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED : ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS; 78 | $configuration->addSetup('setAutoGenerateProxyClasses', [ 79 | $managerConfig->autoGenerateProxyClasses === true ? $defaultStrategy : ProxyFactory::AUTOGENERATE_NEVER, 80 | ]); 81 | } elseif (is_int($managerConfig->autoGenerateProxyClasses)) { 82 | $configuration->addSetup('setAutoGenerateProxyClasses', [$managerConfig->autoGenerateProxyClasses]); 83 | } elseif ($managerConfig->autoGenerateProxyClasses instanceof Statement) { 84 | $configuration->addSetup('setAutoGenerateProxyClasses', [$managerConfig->autoGenerateProxyClasses]); 85 | } 86 | 87 | // Configuration: proxy namespace 88 | if ($managerConfig->proxyNamespace !== null) { 89 | $configuration->addSetup('setProxyNamespace', [$managerConfig->proxyNamespace]); 90 | } 91 | 92 | // Configuration: metadata driver 93 | if ($managerConfig->metadataDriverImpl !== null) { 94 | $configuration->addSetup('setMetadataDriverImpl', [$managerConfig->metadataDriverImpl]); 95 | } else { 96 | // Fallback to ChainMappingDriver 97 | $configuration->addSetup('setMetadataDriverImpl', [$this->prefix(sprintf('@managers.%s.mappingDriver', $managerName))]); 98 | } 99 | 100 | // Configuration: resolve target entities 101 | if ($managerConfig->entityNamespaces !== []) { 102 | $configuration->addSetup('setEntityNamespaces', [$managerConfig->entityNamespaces]); 103 | } 104 | 105 | // Configuration: custom functions 106 | $configuration 107 | ->addSetup('setCustomStringFunctions', [$managerConfig->customStringFunctions]) 108 | ->addSetup('setCustomNumericFunctions', [$managerConfig->customNumericFunctions]) 109 | ->addSetup('setCustomDatetimeFunctions', [$managerConfig->customDatetimeFunctions]) 110 | ->addSetup('setCustomHydrationModes', [$managerConfig->customHydrationModes]); 111 | 112 | // Configuration: class metadata factory name 113 | if ($managerConfig->classMetadataFactoryName !== null) { 114 | $configuration->addSetup('setClassMetadataFactoryName', [$managerConfig->classMetadataFactoryName]); 115 | } 116 | 117 | // Configuration: default repository class name 118 | if ($managerConfig->defaultRepositoryClassName !== null) { 119 | $configuration->addSetup('setDefaultRepositoryClassName', [$managerConfig->defaultRepositoryClassName]); 120 | } 121 | 122 | // Configuration: naming strategy 123 | if ($managerConfig->namingStrategy !== null) { 124 | $configuration->addSetup('setNamingStrategy', [SmartStatement::from($managerConfig->namingStrategy)]); 125 | } 126 | 127 | // Configuration: quote strategy 128 | if ($managerConfig->quoteStrategy !== null) { 129 | $configuration->addSetup('setQuoteStrategy', [SmartStatement::from($managerConfig->quoteStrategy)]); 130 | } 131 | 132 | // Configuration: entity listener resolver 133 | if ($managerConfig->entityListenerResolver !== null) { 134 | $configuration->addSetup('setEntityListenerResolver', [SmartStatement::from($managerConfig->entityListenerResolver)]); 135 | } else { 136 | $configuration->addSetup('setEntityListenerResolver', [$this->prefix('@entityListenerResolver')]); 137 | } 138 | 139 | // Configuration: repository factory 140 | if ($managerConfig->repositoryFactory !== null) { 141 | $configuration->addSetup('setRepositoryFactory', [SmartStatement::from($managerConfig->repositoryFactory)]); 142 | } 143 | 144 | // Configuration: default query hints 145 | if ($managerConfig->defaultQueryHints !== []) { 146 | $configuration->addSetup('setDefaultQueryHints', [$managerConfig->defaultQueryHints]); 147 | } 148 | 149 | // Configuration: filters 150 | if ($managerConfig->filters !== []) { 151 | foreach ($managerConfig->filters as $filterName => $filter) { 152 | $configuration->addSetup('addFilter', [$filterName, $filter->class]); 153 | } 154 | } 155 | 156 | // Configuration: query cache 157 | if ($managerConfig->queryCache !== null) { 158 | $configuration->addSetup('setQueryCache', [SmartStatement::from($managerConfig->queryCache)]); 159 | } elseif ($managerConfig->defaultCache !== null) { 160 | $configuration->addSetup('setQueryCache', [SmartStatement::from($managerConfig->defaultCache)]); 161 | } 162 | 163 | // Configuration: result cache 164 | if ($managerConfig->resultCache !== null) { 165 | $configuration->addSetup('setResultCache', [SmartStatement::from($managerConfig->resultCache)]); 166 | } elseif ($managerConfig->defaultCache !== null) { 167 | $configuration->addSetup('setResultCache', [SmartStatement::from($managerConfig->defaultCache)]); 168 | } 169 | 170 | // Configuration: hydration cache 171 | if ($managerConfig->hydrationCache !== null) { 172 | $configuration->addSetup('setHydrationCache', [SmartStatement::from($managerConfig->hydrationCache)]); 173 | } elseif ($managerConfig->defaultCache !== null) { 174 | $configuration->addSetup('setHydrationCache', [SmartStatement::from($managerConfig->defaultCache)]); 175 | } 176 | 177 | // Configuration: metadata cache 178 | if ($managerConfig->metadataCache !== null) { 179 | $configuration->addSetup('setMetadataCache', [SmartStatement::from($managerConfig->metadataCache)]); 180 | } elseif ($managerConfig->defaultCache !== null) { 181 | $configuration->addSetup('setMetadataCache', [SmartStatement::from($managerConfig->defaultCache)]); 182 | } 183 | 184 | // Configuration: second level cache 185 | if ($managerConfig->secondLevelCache->enabled) { 186 | $cache = $managerConfig->secondLevelCache->cache ?? $managerConfig->defaultCache; 187 | 188 | // Validate second level cache 189 | if ($cache === null) { 190 | throw new LogicalException('Second level cache is enabled but no cache is set.'); 191 | } 192 | 193 | $regionsConfiguration = $builder->addDefinition($this->prefix(sprintf('managers.%s.secondLevelCache.regionsConfiguration', $managerName))) 194 | ->setFactory(RegionsConfiguration::class) 195 | ->setAutowired(false); 196 | 197 | foreach ($managerConfig->secondLevelCache->regions as $regionName => $region) { 198 | $regionsConfiguration->addSetup('setLifetime', [$regionName, $region->lifetime]); 199 | $regionsConfiguration->addSetup('setLockLifetime', [$regionName, $region->lockLifetime]); 200 | } 201 | 202 | $cacheConfiguration = $builder->addDefinition($this->prefix(sprintf('managers.%s.secondLevelCache.cacheConfiguration', $managerName))) 203 | ->setFactory(CacheConfiguration::class) 204 | ->addSetup('setCacheFactory', [ 205 | new Statement(DefaultCacheFactory::class, [ 206 | $regionsConfiguration, 207 | SmartStatement::from($cache), 208 | ]), 209 | ])->addSetup('setRegionsConfiguration', [$regionsConfiguration]) 210 | ->setAutowired(false); 211 | 212 | if ($managerConfig->secondLevelCache->logger !== null) { 213 | $cacheConfiguration->addSetup('setCacheLogger', [SmartStatement::from($managerConfig->secondLevelCache->logger)]); 214 | } 215 | 216 | $configuration->addSetup('setSecondLevelCacheEnabled', [true]); 217 | $configuration->addSetup('setSecondLevelCacheConfiguration', [$cacheConfiguration]); 218 | } 219 | 220 | // Entity Manager 221 | $entityManager = $builder->addDefinition($this->prefix(sprintf('managers.%s.entityManager', $managerName))) 222 | ->setFactory(EntityManager::class, [ 223 | BuilderMan::of($this)->getConnectionByName($managerConfig->connection), // Nettrine/DBAL 224 | $this->prefix(sprintf('@managers.%s.configuration', $managerName)), 225 | ]) 226 | ->addTag(OrmExtension::MANAGER_TAG, ['name' => $managerName]) 227 | ->setAutowired($managerName === 'default'); 228 | 229 | // EntityManager: enable filters 230 | if ($managerConfig->filters !== []) { 231 | foreach ($managerConfig->filters as $filterName => $filter) { 232 | if ($filter->enabled) { 233 | $entityManager->addSetup(new Statement('$service->getFilters()->enable(?)', [$filterName])); 234 | } 235 | } 236 | } 237 | 238 | // EntityManager: decorator class 239 | if ($managerConfig->entityManagerDecoratorClass !== null) { 240 | $builder->addDefinition($this->prefix(sprintf('managers.%s.entityManagerDecorator', $managerName))) 241 | ->setFactory($managerConfig->entityManagerDecoratorClass, [$entityManager]) 242 | ->addTag(OrmExtension::MANAGER_DECORATOR_TAG, ['name' => $managerName]) 243 | ->setAutowired($managerName === 'default'); 244 | 245 | // Disable autowiring for the original EntityManager 246 | $entityManager->setAutowired(false); 247 | } 248 | 249 | // TargetResolvers 250 | if ($managerConfig->resolveTargetEntities !== []) { 251 | $resolver = $builder->addDefinition($this->prefix('targetEntityResolver')) 252 | ->setType(ResolveTargetEntityListener::class) 253 | ->setAutowired(false); 254 | 255 | foreach ($managerConfig->resolveTargetEntities as $name => $implementation) { 256 | $resolver->addSetup('addResolveTargetEntity', [$name, $implementation, []]); 257 | } 258 | } 259 | 260 | // MappingDriver 261 | $mappingDriver = $builder->addDefinition($this->prefix(sprintf('managers.%s.mappingDriver', $managerName))) 262 | ->setFactory(MappingDriverChain::class) 263 | ->addTag(OrmExtension::MAPPING_DRIVER_TAG, ['name' => $managerName]) 264 | ->setAutowired(false); 265 | 266 | // Mapping 267 | foreach ($managerConfig->mapping as $mapping) { 268 | if ($mapping->type === 'attributes') { 269 | $mappingDriver->addSetup('addDriver', [ 270 | new Statement(AttributeDriver::class, [array_values($mapping->directories)]), 271 | $mapping->namespace, 272 | ]); 273 | } elseif ($mapping->type === 'xml') { 274 | $mappingDriver->addSetup('addDriver', [ 275 | new Statement(SimplifiedXmlDriver::class, [array_combine($mapping->directories, array_fill(0, count($mapping->directories), $mapping->namespace))]), 276 | $mapping->namespace, 277 | ]); 278 | } else { 279 | throw new LogicalException(sprintf('Unknown mapping type "%s". Only attribute or xml is supported by default.', $mapping->type)); 280 | } 281 | } 282 | } 283 | 284 | /** 285 | * @phpstan-param TManagerConfig $managerConfig 286 | */ 287 | private function beforeManagerCompile(string $managerName, mixed $managerConfig): void 288 | { 289 | // No-op 290 | } 291 | 292 | } 293 | -------------------------------------------------------------------------------- /src/Events/ContainerEventManager.php: -------------------------------------------------------------------------------- 1 | */ 15 | protected array $initialized = []; 16 | 17 | /** @var array> */ 18 | protected array $listeners = []; 19 | 20 | public function __construct(Container $container) 21 | { 22 | $this->container = $container; 23 | } 24 | 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function dispatchEvent(string $eventName, ?EventArgs $eventArgs = null): void 29 | { 30 | if (!$this->hasListeners($eventName)) { 31 | return; 32 | } 33 | 34 | $eventArgs ??= EventArgs::getEmptyInstance(); 35 | 36 | foreach ($this->getInitializedListeners($eventName) as $listener) { 37 | $callback = [$listener, $eventName]; 38 | assert(is_callable($callback)); 39 | $callback($eventArgs); 40 | } 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function getListeners(string $event): array 47 | { 48 | return $this->getInitializedListeners($event); 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function getAllListeners(): array 55 | { 56 | $result = []; 57 | 58 | foreach ($this->listeners as $eventName => $listeners) { 59 | $result[$eventName] = $this->getInitializedListeners($eventName); 60 | } 61 | 62 | return $result; 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | public function hasListeners(string $event): bool 69 | { 70 | return ($this->listeners[$event] ?? []) !== []; 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | */ 76 | public function addEventListener(array|string $events, string|object $listener): void 77 | { 78 | // Picks the hash code related to that listener 79 | $hash = $this->calculateHash($listener); 80 | 81 | foreach ((array) $events as $event) { 82 | // Overrides listener if a previous one was associated already 83 | // Prevents duplicate listeners on same event (same instance only) 84 | $this->listeners[$event][$hash] = $listener; 85 | } 86 | } 87 | 88 | /** 89 | * {@inheritDoc} 90 | */ 91 | public function removeEventListener(array|string $events, string|object $listener): void 92 | { 93 | // Picks the hash code related to that listener 94 | $hash = $this->calculateHash($listener); 95 | 96 | foreach ((array) $events as $event) { 97 | unset($this->listeners[$event][$hash]); 98 | } 99 | } 100 | 101 | /** 102 | * @return array 103 | */ 104 | private function getInitializedListeners(string $event): array 105 | { 106 | $initialized = $this->initialized[$event] ?? false; 107 | 108 | if ($initialized) { 109 | return $this->listeners[$event] ?? []; // @phpstan-ignore-line 110 | } 111 | 112 | foreach ($this->listeners[$event] ?? [] as $hash => $listener) { 113 | if (!is_object($listener)) { 114 | $this->listeners[$event][$hash] = $this->container->getService($listener); 115 | } 116 | } 117 | 118 | $this->initialized[$event] = true; 119 | 120 | return $this->listeners[$event] ?? []; // @phpstan-ignore-line 121 | } 122 | 123 | private function calculateHash(string|object $listener): string 124 | { 125 | return is_object($listener) ? spl_object_hash($listener) : 'service@' . $listener; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/Exception/LogicalException.php: -------------------------------------------------------------------------------- 1 | registry->getManager($this->registry->getDefaultManagerName()); 21 | assert($manager instanceof EntityManagerInterface); 22 | 23 | return $manager; 24 | } 25 | 26 | public function getManager(string $name): EntityManagerInterface 27 | { 28 | $manager = $this->registry->getManager($name); 29 | assert($manager instanceof EntityManagerInterface); 30 | 31 | return $manager; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/ManagerRegistry.php: -------------------------------------------------------------------------------- 1 | $connectionsMap 17 | * @param array $managersMap 18 | * @param class-string $proxy 19 | */ 20 | public function __construct( 21 | private Container $container, 22 | array $connectionsMap, 23 | array $managersMap, 24 | string $defautConnection = 'default', 25 | string $defaultManager = 'default', 26 | string $proxy = Proxy::class 27 | ) 28 | { 29 | parent::__construct( 30 | 'ORM', 31 | $connectionsMap, 32 | $managersMap, 33 | $defautConnection, 34 | $defaultManager, 35 | $proxy 36 | ); 37 | } 38 | 39 | /** 40 | * @param ObjectManagerDecorator|EntityManager $manager 41 | */ 42 | public static function reopen(ObjectManagerDecorator|EntityManager $manager): void 43 | { 44 | // @phpcs:disable 45 | Binder::use($manager, function (): void { 46 | if ($this instanceof EntityManager) { // @phpstan-ignore-line 47 | $this->closed = false; // @phpstan-ignore-line 48 | } elseif ($this instanceof ObjectManagerDecorator) { 49 | Binder::use($this->wrapped, function (): void { // @phpstan-ignore-line 50 | if ($this instanceof EntityManager) { // @phpstan-ignore-line 51 | $this->closed = false; 52 | } 53 | }); 54 | } 55 | }); 56 | } 57 | 58 | protected function getService(string $name): object 59 | { 60 | return $this->container->getService($name); 61 | } 62 | 63 | protected function resetService(string $name): void 64 | { 65 | $manager = $this->container->getService($name); 66 | 67 | self::reopen($manager); 68 | 69 | $this->container->removeService($name); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Mapping/ContainerEntityListenerResolver.php: -------------------------------------------------------------------------------- 1 | container = $container; 19 | } 20 | 21 | /** 22 | * {@inheritDoc} 23 | */ 24 | public function clear($className = null): void 25 | { 26 | if ($className === null) { 27 | $this->instances = []; 28 | 29 | return; 30 | } 31 | 32 | if (isset($this->instances[$className = trim($className, '\\')])) { 33 | unset($this->instances[$className]); 34 | } 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function register($object): void // @phpstan-ignore missingType.parameter 41 | { 42 | $this->instances[$object::class] = $object; 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function resolve($className): object 49 | { 50 | /** @var class-string $className */ 51 | $className = trim($className, '\\'); 52 | 53 | if (isset($this->instances[$className])) { 54 | return $this->instances[$className]; 55 | } 56 | 57 | $service = $this->container->getByType($className, false); 58 | 59 | $this->instances[$className] = $service ?? new $className(); 60 | 61 | return $this->instances[$className]; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/Utils/Binder.php: -------------------------------------------------------------------------------- 1 | bindTo(is_object($objectOrClass) ? $objectOrClass : null, $objectOrClass)(); 16 | } 17 | 18 | } 19 | --------------------------------------------------------------------------------