From c4d917375158229227a976f8cd9613a69662c7c9 Mon Sep 17 00:00:00 2001 From: Jan Tvrdik Date: Fri, 12 Feb 2021 23:12:24 +0100 Subject: [PATCH] add SymfonyBundle [closes #378] --- composer.json | 3 + .../DependencyInjection/Configuration.php | 30 +++ .../NextrasOrmExtension.php | 241 ++++++++++++++++++ .../SymfonyBundle/NextrasOrmBundle.php | 11 + .../SymfonyBundle/RepositoryLoader.php | 44 ++++ 5 files changed, 329 insertions(+) create mode 100644 src/Bridges/SymfonyBundle/DependencyInjection/Configuration.php create mode 100644 src/Bridges/SymfonyBundle/DependencyInjection/NextrasOrmExtension.php create mode 100644 src/Bridges/SymfonyBundle/NextrasOrmBundle.php create mode 100644 src/Bridges/SymfonyBundle/RepositoryLoader.php diff --git a/composer.json b/composer.json index b3822d95..f4eab361 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,9 @@ "phpstan/phpstan-nette": "0.12.14", "phpstan/phpstan-mockery": "0.12.11", "phpstan/phpstan-strict-rules": "0.12.7", + "symfony/config": "~4.4 || ~5.0", + "symfony/dependency-injection": "~4.4 || ~5.0", + "symfony/http-kernel": "~4.4 || ~5.0", "nextras/orm-phpstan": "dev-master", "marc-mabe/php-enum-phpstan": "dev-master", "tracy/tracy": "~2.3" diff --git a/src/Bridges/SymfonyBundle/DependencyInjection/Configuration.php b/src/Bridges/SymfonyBundle/DependencyInjection/Configuration.php new file mode 100644 index 00000000..ac38ad76 --- /dev/null +++ b/src/Bridges/SymfonyBundle/DependencyInjection/Configuration.php @@ -0,0 +1,30 @@ +getRootNode(); + assert($root instanceof ArrayNodeDefinition); + + // @formatter:off + $root + ->children() + ->scalarNode('model') + ->isRequired() + ->end(); + // @formatter:on + + return $treeBuilder; + } +} diff --git a/src/Bridges/SymfonyBundle/DependencyInjection/NextrasOrmExtension.php b/src/Bridges/SymfonyBundle/DependencyInjection/NextrasOrmExtension.php new file mode 100644 index 00000000..c7022b4a --- /dev/null +++ b/src/Bridges/SymfonyBundle/DependencyInjection/NextrasOrmExtension.php @@ -0,0 +1,241 @@ + $configs + */ + public function load(array $configs, ContainerBuilder $builder): void + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $modelClass = $config['model']; + $repositories = $this->findRepositories($builder, $modelClass); + $modelConfig = Model::getConfiguration($repositories); + + foreach ($repositories as $repositoryName => $repositoryClass) { + $mapperClass = str_replace('Repository', 'Mapper', $repositoryClass); + $this->setupMapper($builder, $mapperClass); + $this->setupRepository($builder, $repositoryClass, $mapperClass, $modelClass); + } + + $this->setupCacheStorage($builder); + $this->setupCache($builder); + $this->setupDbalMapperCoordinator($builder); + $this->setupRepositoryLoader($builder); + $this->setupMetadataParserFactory($builder); + $this->setupMetadataStorage($builder, $modelConfig[2]); + $this->setupModel($builder, $modelClass, $modelConfig); + } + + + /** + * @return array + * @phpstan-param class-string<\Nextras\Orm\Model\IModel> $modelClass + * @phpstan-return array>> + */ + protected function findRepositories(ContainerBuilder $builder, string $modelClass): array + { + if ($modelClass === Model::class) { + throw new InvalidStateException('Your model has to inherit from ' . Model::class . '. Use compiler extension configuration - model key.'); + } + + $modelReflection = new ReflectionClass($modelClass); + $classFileName = $modelReflection->getFileName(); + assert($classFileName !== false); + $builder->addResource(new FileResource($classFileName)); + + $repositories = []; + preg_match_all( + '~^ [ \t*]* @property(?:|-read) [ \t]+ ([^\s$]+) [ \t]+ \$ (\w+) ()~mx', + (string) $modelReflection->getDocComment(), $matches, PREG_SET_ORDER + ); + + /** + * @var string $type + * @var string $name + */ + foreach ($matches as [, $type, $name]) { + /** @phpstan-var class-string> $type */ + $type = Reflection::expandClassName($type, $modelReflection); + if (!class_exists($type)) { + throw new RuntimeException("Repository '{$type}' does not exist."); + } + + $rc = new ReflectionClass($type); + assert($rc->implementsInterface(IRepository::class), sprintf( + 'Property "%s" of class "%s" with type "%s" does not implement interface %s.', + $modelClass, $name, $type, IRepository::class + )); + + $repositories[$name] = $type; + } + + return $repositories; + } + + + private function setupCacheStorage(ContainerBuilder $builder): void + { + if ($builder->has(IStorage::class)) { + return; + } + + $definition = new Definition(FileStorage::class); + $definition->setArgument('$dir', $builder->getParameter('kernel.cache_dir')); + + $builder->setDefinition(FileStorage::class, $definition); + $builder->setAlias(IStorage::class, FileStorage::class); + } + + + private function setupCache(ContainerBuilder $builder): void + { + if ($builder->has(Cache::class)) { + return; + } + + $definition = new Definition(Cache::class); + $definition->setArgument('$storage', new Reference(IStorage::class)); + $definition->setArgument('$namespace', 'nextras_orm'); + + $builder->setDefinition(Cache::class, $definition); + } + + + private function setupDbalMapperCoordinator(ContainerBuilder $builder): void + { + if ($builder->has(DbalMapperCoordinator::class)) { + return; + } + + $definition = new Definition(DbalMapperCoordinator::class); + $definition->setArgument('$connection', new Reference(IConnection::class)); + + $builder->setDefinition(DbalMapperCoordinator::class, $definition); + } + + + private function setupMapper(ContainerBuilder $builder, string $mapperClass): void + { + if ($builder->has($mapperClass)) { + return; + } + + $definition = new Definition($mapperClass); + $definition->setArgument('$connection', new Reference(IConnection::class)); + $definition->setArgument('$mapperCoordinator', new Reference(DbalMapperCoordinator::class)); + $definition->setArgument('$cache', new Reference(Cache::class)); + + $builder->setDefinition($mapperClass, $definition); + } + + + private function setupRepository(ContainerBuilder $builder, string $repositoryClass, string $mapperClass, string $modelClass): void + { + if ($builder->has($repositoryClass)) { + return; + } + + $definition = new Definition($repositoryClass); + $definition->setArgument('$mapper', new Reference($mapperClass)); + $definition->addMethodCall('setModel', [new Reference($modelClass)]); + $definition->setPublic(true); + + $builder->setDefinition($repositoryClass, $definition); + } + + + private function setupRepositoryLoader(ContainerBuilder $builder): void + { + if ($builder->has(IRepositoryLoader::class)) { + return; + } + + $definition = new Definition(RepositoryLoader::class); + $definition->setArgument('$container', new Reference(ContainerInterface::class)); + + $builder->setDefinition(RepositoryLoader::class, $definition); + $builder->setAlias(IRepositoryLoader::class, RepositoryLoader::class); + } + + + private function setupMetadataParserFactory(ContainerBuilder $builder): void + { + if ($builder->has(IMetadataParserFactory::class)) { + return; + } + + $definition = new Definition(MetadataParserFactory::class); + + $builder->setDefinition(MetadataParserFactory::class, $definition); + $builder->setAlias(IMetadataParserFactory::class, MetadataParserFactory::class); + } + + + /** + * @param array $entityClassMap + */ + private function setupMetadataStorage(ContainerBuilder $builder, array $entityClassMap): void + { + if ($builder->has(MetadataStorage::class)) { + return; + } + + $definition = new Definition(MetadataStorage::class); + $definition->setArgument('$entityClassesMap', $entityClassMap); + $definition->setArgument('$cache', new Reference(Cache::class)); + $definition->setArgument('$metadataParserFactory', new Reference(IMetadataParserFactory::class)); + $definition->setArgument('$repositoryLoader', new Reference(IRepositoryLoader::class)); + + $builder->setDefinition(MetadataStorage::class, $definition); + } + + + /** + * @param mixed[] $config + */ + private function setupModel(ContainerBuilder $builder, string $modelClass, array $config): void + { + if ($builder->has($modelClass)) { + return; + } + + $definition = new Definition($modelClass); + $definition->setArgument('$configuration', $config); + $definition->setArgument('$repositoryLoader', new Reference(IRepositoryLoader::class)); + $definition->setArgument('$metadataStorage', new Reference(MetadataStorage::class)); + + $builder->setDefinition($modelClass, $definition); + $builder->setAlias(IModel::class, $modelClass); + } +} diff --git a/src/Bridges/SymfonyBundle/NextrasOrmBundle.php b/src/Bridges/SymfonyBundle/NextrasOrmBundle.php new file mode 100644 index 00000000..8667ec67 --- /dev/null +++ b/src/Bridges/SymfonyBundle/NextrasOrmBundle.php @@ -0,0 +1,11 @@ +container = $container; + } + + + public function hasRepository(string $className): bool + { + return $this->container->has($className); + } + + + /** + * @phpstan-template R of IRepository<\Nextras\Orm\Entity\IEntity> + * @phpstan-param class-string $className + * @phpstan-return R + */ + public function getRepository(string $className): IRepository + { + /** @phpstan-var R */ + return $this->container->get($className); + } + + + public function isCreated(string $className): bool + { + return $this->container->initialized($className); + } +}