diff --git a/src/Bridges/NetteDI/DIRepositoryFinder.php b/src/Bridges/NetteDI/DIRepositoryFinder.php index 388a5af1..ec87f04d 100644 --- a/src/Bridges/NetteDI/DIRepositoryFinder.php +++ b/src/Bridges/NetteDI/DIRepositoryFinder.php @@ -19,7 +19,12 @@ class DIRepositoryFinder implements IRepositoryFinder // @phpstan-ignore-next-line https://github.com/phpstan/phpstan/issues/587 - public function __construct(string $modelClass, ContainerBuilder $containerBuilder, OrmExtension $extension) + public function __construct( + string $modelClass, + protected readonly array $extensions, + ContainerBuilder $containerBuilder, + OrmExtension $extension, + ) { $this->builder = $containerBuilder; $this->extension = $extension; @@ -87,6 +92,7 @@ protected function setupRepositoryLoader(array $repositoriesMap): void ->setType(RepositoryLoader::class) ->setArguments([ 'repositoryNamesMap' => $repositoriesMap, + 'extensions' => $this->extensions, ]); } diff --git a/src/Bridges/NetteDI/IRepositoryFinder.php b/src/Bridges/NetteDI/IRepositoryFinder.php index 525bf3ef..1f252d7a 100644 --- a/src/Bridges/NetteDI/IRepositoryFinder.php +++ b/src/Bridges/NetteDI/IRepositoryFinder.php @@ -4,6 +4,7 @@ use Nette\DI\ContainerBuilder; +use Nette\DI\Definitions\Statement; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Model\IModel; use Nextras\Orm\Repository\IRepository; @@ -13,8 +14,9 @@ interface IRepositoryFinder { /** * @param class-string $modelClass + * @param list $extensions */ - public function __construct(string $modelClass, ContainerBuilder $containerBuilder, OrmExtension $extension); + public function __construct(string $modelClass, array $extensions, ContainerBuilder $containerBuilder, OrmExtension $extension); /** diff --git a/src/Bridges/NetteDI/OrmExtension.php b/src/Bridges/NetteDI/OrmExtension.php index 095b396c..104b4d93 100644 --- a/src/Bridges/NetteDI/OrmExtension.php +++ b/src/Bridges/NetteDI/OrmExtension.php @@ -6,13 +6,14 @@ use Nette\Caching\Cache; use Nette\DI\CompilerExtension; use Nette\DI\ContainerBuilder; +use Nette\DI\Definitions\Statement; use Nette\Schema\Expect; use Nette\Schema\Schema; use Nextras\Dbal\IConnection; use Nextras\Orm\Entity\IEntity; -use Nextras\Orm\Entity\Reflection\IMetadataParserFactory; -use Nextras\Orm\Entity\Reflection\MetadataParser; +use Nextras\Orm\Entity\Reflection\MetadataParserFactory; use Nextras\Orm\Exception\InvalidStateException; +use Nextras\Orm\Extension; use Nextras\Orm\Mapper\Dbal\DbalMapperCoordinator; use Nextras\Orm\Model\IModel; use Nextras\Orm\Model\MetadataStorage; @@ -39,6 +40,7 @@ public function getConfigSchema(): Schema { return Expect::structure([ 'model' => Expect::string()->default(Model::class), + 'extensions' => Expect::arrayOf('string|Nette\DI\Definitions\Statement')->default([]), 'repositoryFinder' => Expect::string()->default(PhpDocRepositoryFinder::class), 'initializeMetadata' => Expect::bool()->default(false), 'autowiredInternalServices' => Expect::bool()->default(true), @@ -52,23 +54,28 @@ public function loadConfiguration(): void $this->builder = $this->getContainerBuilder(); $this->modelClass = $this->config->model; + $extensions = []; + foreach ($this->config->extensions as $extension) { + $extensions[] = is_string($extension) ? new Statement($extension) : $extension; + } + $repositoryFinderClass = $this->config->repositoryFinder; if (!is_subclass_of($repositoryFinderClass, IRepositoryFinder::class)) { throw new InvalidStateException('Repository finder does not implement Nextras\Orm\Bridges\NetteDI\IRepositoryFinder interface.'); } - $this->repositoryFinder = new $repositoryFinderClass($this->modelClass, $this->builder, $this); + $this->repositoryFinder = new $repositoryFinderClass($this->modelClass, $extensions, $this->builder, $this); $repositories = $this->repositoryFinder->loadConfiguration(); $this->setupCache(); $this->setupDependencyProvider(); $this->setupDbalMapperDependencies(); - $this->setupMetadataParserFactory(); + $this->setupMetadataParserFactory($extensions); if ($repositories !== null) { $repositoriesConfig = Model::getConfiguration($repositories); $this->setupMetadataStorage($repositoriesConfig[2]); - $this->setupModel($this->modelClass, $repositoriesConfig); + $this->setupModel($this->modelClass, $repositoriesConfig, $extensions); } $this->initializeMetadata($this->config->initializeMetadata); @@ -80,9 +87,14 @@ public function beforeCompile(): void $repositories = $this->repositoryFinder->beforeCompile(); if ($repositories !== null) { + $extensions = []; + foreach ($this->config->extensions as $extension) { + $extensions[] = is_string($extension) ? new Statement($extension) : $extension; + } + $repositoriesConfig = Model::getConfiguration($repositories); $this->setupMetadataStorage($repositoriesConfig[2]); - $this->setupModel($this->modelClass, $repositoriesConfig); + $this->setupModel($this->modelClass, $repositoriesConfig, $extensions); } $this->setupDbalMapperDependencies(); @@ -138,18 +150,19 @@ protected function setupDbalMapperDependencies(): void } - protected function setupMetadataParserFactory(): void + /** + * @param list $extensions + */ + protected function setupMetadataParserFactory(array $extensions): void { $factoryName = $this->prefix('metadataParserFactory'); if ($this->builder->hasDefinition($factoryName)) { return; } - $this->builder->addFactoryDefinition($factoryName) - ->setImplement(IMetadataParserFactory::class) - ->getResultDefinition() - ->setType(MetadataParser::class) - ->setArguments(['$entityClassesMap']) + $this->builder->addDefinition($factoryName) + ->setType(MetadataParserFactory::class) + ->setArgument('extensions', $extensions) ->setAutowired($this->config->autowiredInternalServices); } @@ -182,8 +195,9 @@ protected function setupMetadataStorage(array $entityClassMap): void * array>>, * array, class-string>> * } $repositoriesConfig + * @param list $extensions */ - protected function setupModel(string $modelClass, array $repositoriesConfig): void + protected function setupModel(string $modelClass, array $repositoriesConfig, array $extensions): void { $modelName = $this->prefix('model'); if ($this->builder->hasDefinition($modelName)) { @@ -196,7 +210,8 @@ protected function setupModel(string $modelClass, array $repositoriesConfig): vo 'configuration' => $repositoriesConfig, 'repositoryLoader' => $this->prefix('@repositoryLoader'), 'metadataStorage' => $this->prefix('@metadataStorage'), - ]); + ]) + ->addSetup('foreach (? as $e) { $e->configureModel($service); }', [$extensions]); } diff --git a/src/Bridges/NetteDI/PhpDocRepositoryFinder.php b/src/Bridges/NetteDI/PhpDocRepositoryFinder.php index 54c3df9f..3a04b461 100644 --- a/src/Bridges/NetteDI/PhpDocRepositoryFinder.php +++ b/src/Bridges/NetteDI/PhpDocRepositoryFinder.php @@ -18,6 +18,7 @@ class PhpDocRepositoryFinder implements IRepositoryFinder { public function __construct( protected readonly string $modelClass, + protected readonly array $extensions, protected readonly ContainerBuilder $builder, protected readonly OrmExtension $extension, ) @@ -143,6 +144,7 @@ protected function setupRepositoryLoader(array $repositoriesMap): void ->setType(RepositoryLoader::class) ->setArguments([ 'repositoryNamesMap' => $repositoriesMap, + 'extensions' => $this->extensions, ]); } } diff --git a/src/Bridges/NetteDI/RepositoryLoader.php b/src/Bridges/NetteDI/RepositoryLoader.php index 2a9fafa9..7bcb09cc 100644 --- a/src/Bridges/NetteDI/RepositoryLoader.php +++ b/src/Bridges/NetteDI/RepositoryLoader.php @@ -5,18 +5,25 @@ use Nette\DI\Container; use Nextras\Orm\Entity\IEntity; +use Nextras\Orm\Extension; use Nextras\Orm\Model\IRepositoryLoader; use Nextras\Orm\Repository\IRepository; class RepositoryLoader implements IRepositoryLoader { + /** @var array */ + private array $configuredRepositories = []; + + /** * @param array>, string> $repositoryNamesMap + * @param list $extensions */ public function __construct( private readonly Container $container, private readonly array $repositoryNamesMap, + private readonly array $extensions, ) { } @@ -37,7 +44,17 @@ public function hasRepository(string $className): bool public function getRepository(string $className): IRepository { /** @var R */ - return $this->container->getService($this->repositoryNamesMap[$className]); + $repository = $this->container->getService($this->repositoryNamesMap[$className]); + + if (!isset($this->configuredRepositories[$className])) { + $this->configuredRepositories[$className] = true; + foreach ($this->extensions as $extensions) { + $extensions->configureRepository($repository); + $extensions->configureMapper($repository->getMapper()); + } + } + + return $repository; } diff --git a/src/Entity/Reflection/MetadataParser.php b/src/Entity/Reflection/MetadataParser.php index 97d72c97..faec2085 100644 --- a/src/Entity/Reflection/MetadataParser.php +++ b/src/Entity/Reflection/MetadataParser.php @@ -18,6 +18,7 @@ use Nextras\Orm\Entity\PropertyWrapper\PrimaryProxyWrapper; use Nextras\Orm\Exception\InvalidStateException; use Nextras\Orm\Exception\NotSupportedException; +use Nextras\Orm\Extension; use Nextras\Orm\Relationships\HasMany; use Nextras\Orm\Relationships\ManyHasMany; use Nextras\Orm\Relationships\ManyHasOne; @@ -94,8 +95,12 @@ class MetadataParser implements IMetadataParser /** * @param array $entityClassesMap * @param array, class-string>> $entityClassesMap + * @param list $extensions */ - public function __construct(array $entityClassesMap) + public function __construct( + array $entityClassesMap, + protected array $extensions = [], + ) { $this->entityClassesMap = $entityClassesMap; $this->modifierParser = new ModifierParser(); @@ -135,6 +140,10 @@ public function parseMetadata(string $entityClass, array|null &$fileDependencies $this->loadProperties($fileDependencies); $this->initPrimaryKey(); + foreach ($this->extensions as $extension) { + $extension->configureEntityMetadata($this->metadata); + } + if ($fileDependencies !== null) { $fileDependencies = array_values(array_unique($fileDependencies)); } @@ -239,6 +248,11 @@ protected function parseProperty( $this->parseAnnotationValue($property, $propertyNode->description); $this->processPropertyGettersSetters($property, $methods); $this->processDefaultPropertyWrappers($property); + + foreach ($this->extensions as $extension) { + $extension->configureEntityPropertyMetadata($this->metadata, $property, $propertyNode->type); + } + return $property; } diff --git a/src/Entity/Reflection/MetadataParserFactory.php b/src/Entity/Reflection/MetadataParserFactory.php index 1b01b8a5..a992ed02 100644 --- a/src/Entity/Reflection/MetadataParserFactory.php +++ b/src/Entity/Reflection/MetadataParserFactory.php @@ -3,10 +3,21 @@ namespace Nextras\Orm\Entity\Reflection; +use Nextras\Orm\Extension; + + class MetadataParserFactory implements IMetadataParserFactory { + /** @param list $extensions */ + public function __construct( + private readonly array $extensions = [], + ) + { + } + + public function create(array $entityClassesMap): IMetadataParser { - return new MetadataParser($entityClassesMap); + return new MetadataParser($entityClassesMap, $this->extensions); } } diff --git a/src/Extension.php b/src/Extension.php new file mode 100644 index 00000000..2b87af6a --- /dev/null +++ b/src/Extension.php @@ -0,0 +1,86 @@ + $repository + */ + public function configureRepository( + IRepository $repository, + ): void + { + } + + + /** + * Modifies the mapper instance. + * + * Runs every time the mapper is instantiated in runtime. + * + * @param IMapper<*> $mapper + */ + public function configureMapper( + IMapper $mapper, + ): void + { + } + + + /** + * Modifies the entity metadata instance. + * + * Runs when entity property metadata are parsed during compile time (before cache serialization). + */ + public function configureEntityMetadata( + EntityMetadata $metadata, + ): void + { + } + + + /** + * Modifies the entity property metadata instance. + * + * Runs when entity property metadata are parsed during compile time (before cache serialization). + */ + public function configureEntityPropertyMetadata( + EntityMetadata $entityMetadata, + PropertyMetadata $propertyMetadata, + TypeNode $propertyType, + ): void + { + } +} diff --git a/src/Model/SimpleModelFactory.php b/src/Model/SimpleModelFactory.php index 8d320897..74392261 100644 --- a/src/Model/SimpleModelFactory.php +++ b/src/Model/SimpleModelFactory.php @@ -7,6 +7,7 @@ use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\Reflection\IMetadataParserFactory; use Nextras\Orm\Entity\Reflection\MetadataParserFactory; +use Nextras\Orm\Extension; use Nextras\Orm\Repository\IRepository; use function array_values; @@ -17,11 +18,13 @@ class SimpleModelFactory * @template E of IEntity * @param array> $repositories Map of a repository name and its instance. The name is used * for accessing repository by name in contrast to accessing by class-string. + * @param list $extensions */ public function __construct( private readonly Cache $cache, private readonly array $repositories, private readonly IMetadataParserFactory|null $metadataParserFactory = null, + private readonly array $extensions = [], ) { } @@ -30,7 +33,7 @@ public function __construct( public function create(): Model { $config = Model::getConfiguration($this->repositories); - $parser = $this->metadataParserFactory ?? new MetadataParserFactory(); + $parser = $this->metadataParserFactory ?? new MetadataParserFactory($this->extensions); $loader = new SimpleRepositoryLoader(array_values($this->repositories)); $metadata = new MetadataStorage($config[2], $this->cache, $parser, $loader); $model = new Model($config, $loader, $metadata); @@ -39,6 +42,14 @@ public function create(): Model $repository->setModel($model); } + foreach ($this->extensions as $extension) { + $extension->configureModel($model); + foreach ($this->repositories as $repository) { + $extension->configureRepository($repository); + $extension->configureMapper($repository->getMapper()); + } + } + return $model; } } diff --git a/tests/config.array.neon b/tests/config.array.neon index 6c2489f6..c001b55a 100644 --- a/tests/config.array.neon +++ b/tests/config.array.neon @@ -4,6 +4,9 @@ extensions: nextras.orm: model: NextrasTests\Orm\Model repositoryFinder: Nextras\Orm\Bridges\NetteDI\TestMapperPhpDocRepositoryFinder + extensions: + - @NextrasTests\Orm\TestExtension services: - Nextras\Orm\TestHelper\EntityCreator + - NextrasTests\Orm\TestExtension diff --git a/tests/inc/TestExtension.php b/tests/inc/TestExtension.php new file mode 100644 index 00000000..4ff3e8b2 --- /dev/null +++ b/tests/inc/TestExtension.php @@ -0,0 +1,21 @@ +onAfterInsert[] = function (IEntity $entity) { + dump("Publisher {$entity->getPersistedId()} inserted."); + }; + } + } +}