diff --git a/docs/config-nette.md b/docs/config-nette.md index 0c8c77625..71d0aa45b 100644 --- a/docs/config-nette.md +++ b/docs/config-nette.md @@ -105,6 +105,15 @@ services: nextras.orm.dependencyProvider: MyApp\DependencyProvider ``` +To hook into the model, repository, mapper, or entity metadata setup, register an [extension](extensions) through the `extensions` option: + +```neon +nextras.orm: + extensions: + - MyApp\MyExtension + - @myAlreadyRegisteredExtension +``` + Orm sets up all internal services as autowired. This may be toggled by the `autowiredInternalServices` option. This may be useful, especially when the Orm extension is used multiple times. The `connection` option allows specifying the related connection instance. ```neon diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 000000000..d7c715d4b --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,89 @@ +## Extensions + +An extension is an entry point for hooking into Orm's setup. It lets bundled or third-party packages consistently configure the model, repositories, mappers, and entity metadata without patching your application code. Typical use cases: + +- registering custom metadata modifiers or reacting to parsed entity metadata (e.g. adding behavior based on a custom property modifier), +- attaching event callbacks to every repository, +- injecting shared dependencies or configuration into mappers. + +To implement an extension, extend the `Nextras\Orm\Extension` abstract class and override only the methods you need. All methods have an empty default implementation. + +```php +use Nextras\Orm\Extension; +use Nextras\Orm\Model\IModel; +use Nextras\Orm\Repository\IRepository; + +class MyExtension extends Extension +{ + public function configureModel(IModel $model): void + { + $model->onFlush[] = function ($persisted, $removed) { + // ... + }; + } + + public function configureRepository(IRepository $repository): void + { + $repository->onBeforePersist[] = function ($entity) { + // ... + }; + } + + // e.g. assign a custom property wrapper to every property of a given type + public function configureEntityPropertyMetadata($entityMetadata, $propertyMetadata, $propertyType): void + { + if ($propertyMetadata->wrapper !== null) return; + foreach ($propertyMetadata->types as $type => $_) { + if ($type === LocalDate::class || is_subclass_of($type, LocalDate::class)) { + $propertyMetadata->wrapper = LocalDatePropertyWrapper::class; + } + } + } +} +``` + +#### Hooks + +| Method | Runs | +|-------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------| +| `configureModel(IModel $model)` | Once, when the model is instantiated. | +| `configureRepository(IRepository $repository)` | Every time a repository is instantiated at runtime. | +| `configureMapper(IMapper $mapper)` | Every time a mapper is instantiated at runtime. | +| `configureEntityMetadata(EntityMetadata $metadata)` | At compile time, when entity metadata is parsed (before it is cached). | +| `configureEntityPropertyMetadata(EntityMetadata $entityMetadata, PropertyMetadata $propertyMetadata, TypeNode $propertyType)` | At compile time, per property, when entity metadata is parsed (before it is cached). | + +The runtime hooks (`configureModel`, `configureRepository`, `configureMapper`) run each time the respective service is created, so keep them cheap. The compile-time hooks (`configureEntityMetadata`, `configureEntityPropertyMetadata`) run only while metadata is parsed and their result is serialized into the metadata cache — they do **not** run again on subsequent requests served from the cache. Because of that, do not capture request-specific state in them. + +#### Registration with Nette DI + +Register extensions through the `extensions` option of the [Nette DI](config-nette) `OrmExtension`. Each entry may be: + +- a class name — a new service is registered and instantiated once, +- a `Nette\DI\Definitions\Statement` — a factory for a new service, +- a `@reference` — an already registered service is reused. + +```neon +nextras.orm: + model: MyApp\Model + extensions: + - MyApp\MyExtension + - MyApp\ConfigurableExtension(%myOption%) + - @myAlreadyRegisteredExtension +``` + +Each extension is instantiated once and shared across the metadata parser, the model, and every repository. + +#### Registration without Nette DI + +When bootstrapping the model manually via `Nextras\Orm\Model\SimpleModelFactory`, pass the extension instances as the last constructor argument: + +```php +use Nextras\Orm\Model\SimpleModelFactory; + +$factory = new SimpleModelFactory( + $cache, + $repositories, + extensions: [new MyApp\MyExtension()], +); +$model = $factory->create(); +``` diff --git a/docs/menu.md b/docs/menu.md index 950ae814d..f74d33ddf 100644 --- a/docs/menu.md +++ b/docs/menu.md @@ -16,6 +16,7 @@ - [Mapper](mapper) - [Conventions](conventions) - [Events](events) +- [Extensions](extensions) ###### Tutorials: diff --git a/docs/migrate_6.0.md b/docs/migrate_6.0.md index 24f977e9e..c92831800 100644 --- a/docs/migrate_6.0.md +++ b/docs/migrate_6.0.md @@ -11,3 +11,66 @@ - `Nextras\Orm\Entity\ImmutableValuePropertyWrapper` already implements this: it keeps the raw value, converts it lazily via `convertFromRawValue()` on read, and validates nullability centrally. Your `convertFromRawValue()` is free to validate the (converted) value as before — it is simply invoked lazily now. As a consequence, an invalid raw value now surfaces its exception when the value is **read** rather than when it is **set** (i.e. during hydration / `setRawValue()`). + +- **`Model` constructor changed & `Model::getConfiguration()` removed** - the repository configuration array is gone. The model now delegates all repository lookups to its `IRepositoryLoader`. + + ```php + // before + new Model($configuration, $repositoryLoader, $metadataStorage); + Model::getConfiguration($repositories); // removed + + // after + new Model($repositoryLoader, $metadataStorage); + ``` + + This affects you only if you instantiate `Model` manually (i.e. not via the Nette DI extension or `SimpleModelFactory`). The entity-to-repository and name-to-repository maps are now owned by the loader (see below). + +- **`IRepositoryLoader` interface reworked** - if you maintain a custom repository loader, update it to the new contract: + - `getRepository()` now returns `IRepository|null` (previously non-nullable). The `Model` throws when it receives `null`. + - `isCreated()` was **removed**; implement `getInitializedRepositories(): list>` instead (returns the already-instantiated repositories, used by `flush()` / `clear()` / `refreshAll()`). + - Added `hasRepositoryByName(string $name): bool` and `getRepositoryByName(string $name): IRepository|null`. + - Added `getRepositoryClassNameForEntity(string $entityClassName): ?string` - resolves the repository managing a given entity class (previously the `Model` held this map itself). + +- **Removed `Nextras\Orm\Bridges\NetteDI\RepositoryLoader`** - replaced by `Nextras\Orm\Bridges\NetteDI\DiRepositoryLoader`, which implements the new `IRepositoryLoader` contract and reads repositories lazily from the Nette DI container. + +- **`IRepositoryFinder` interface reworked** - if you implement a custom repository finder for the Nette DI bridge: + - Constructor arguments were reordered to `__construct(ContainerBuilder $builder, OrmExtension $extension, string $modelClass)`. + - `loadConfiguration(): ?array` was replaced by `registerRepositories(): void` (register your repository service definitions here). + - `beforeCompile(): ?array` was replaced by `resolveRepositories(): list` (return the resolved repository service definitions; the `OrmExtension` wires them to the model). See the new `Nextras\Orm\Bridges\NetteDI\DiRepositoryEntry` value object. + +- **`MetadataParserFactory` constructor gained `$extensions`** - it now accepts `list` as its first constructor argument and forwards them to the `MetadataParser`. If you register a custom `nextras.orm.metadataParserFactory` service or construct the factory manually, account for the new argument. + +- **`SimpleModelFactory` / `SimpleRepositoryLoader` signatures changed** (relevant for non-Nette, manual model bootstrapping): + - `SimpleModelFactory::__construct()` gained a trailing `array $extensions = []` parameter (`list`). + - `SimpleRepositoryLoader::__construct()` gained a second `array $entityClassNameToClassNameMap = []` parameter (map of entity class name → managing repository class name), needed for `getRepositoryClassNameForEntity()`. + +### New Features + +- **`Nextras\Orm\Extension` entry point** - a new abstract class that lets bundled or third-party packages hook into the Orm setup. Override any of the methods you need and register the extension: + + ```php + class MyExtension extends \Nextras\Orm\Extension + { + public function configureModel(IModel $model): void { /* ... */ } + public function configureRepository(IRepository $repository): void { /* ... */ } + public function configureMapper(IMapper $mapper): void { /* ... */ } + public function configureEntityMetadata(EntityMetadata $metadata): void { /* ... */ } + public function configureEntityPropertyMetadata( + EntityMetadata $entityMetadata, + PropertyMetadata $propertyMetadata, + TypeNode $propertyType, + ): void { /* ... */ } + } + ``` + + With the Nette DI bridge, register extensions through the `extensions` option. Each entry may be a class name, a `Nette\DI\Definitions\Statement`, or a `@reference` to an already registered service: + + ```neon + nextras.orm: + model: MyApp\Model + extensions: + - MyApp\MyExtension + - @myAlreadyRegisteredExtension + ``` + + The `configureModel`/`configureRepository`/`configureMapper` hooks run when the respective service is instantiated; the `configureEntityMetadata`/`configureEntityPropertyMetadata` hooks run at compile time while entity metadata is parsed (before it is cached). diff --git a/src/Bridges/NetteDI/DIRepositoryFinder.php b/src/Bridges/NetteDI/DIRepositoryFinder.php index ec87f04db..938e97c96 100644 --- a/src/Bridges/NetteDI/DIRepositoryFinder.php +++ b/src/Bridges/NetteDI/DIRepositoryFinder.php @@ -4,104 +4,78 @@ use Nette\DI\ContainerBuilder; -use Nette\DI\Definitions\Definition; use Nette\DI\Definitions\FactoryDefinition; use Nette\DI\Definitions\ServiceDefinition; -use Nextras\Orm\Entity\IEntity; -use Nextras\Orm\Exception\InvalidStateException; +use Nextras\Orm\Exception\InvalidArgumentException; +use Nextras\Orm\Exception\NotSupportedException; use Nextras\Orm\Repository\IRepository; class DIRepositoryFinder implements IRepositoryFinder { - private ContainerBuilder $builder; - private OrmExtension $extension; - - - // @phpstan-ignore-next-line https://github.com/phpstan/phpstan/issues/587 + #[\Override] public function __construct( - string $modelClass, - protected readonly array $extensions, - ContainerBuilder $containerBuilder, - OrmExtension $extension, + protected readonly ContainerBuilder $builder, + protected readonly OrmExtension $extension, + protected readonly string $modelClass, ) { - $this->builder = $containerBuilder; - $this->extension = $extension; } - public function loadConfiguration(): ?array + #[\Override] + public function registerRepositories(): void { - return null; } - public function beforeCompile(): ?array + #[\Override] + public function resolveRepositories(): array { - $types = $this->findRepositories(); $repositories = []; - $repositoriesMap = []; - foreach ($types as $serviceName => $serviceDefinition) { - $serviceName = (string) $serviceName; - if ($serviceDefinition instanceof FactoryDefinition) { - $serviceDefinition - ->getResultDefinition() - ->addSetup('setModel', [$this->extension->prefix('@model')]); - $name = $this->getRepositoryName($serviceName, $serviceDefinition); - - } elseif ($serviceDefinition instanceof ServiceDefinition || $serviceDefinition instanceof \Nette\DI\ServiceDefinition) { // @phpstan-ignore-line - $serviceDefinition - ->addSetup('setModel', [$this->extension->prefix('@model')]); - $name = $this->getRepositoryName($serviceName, $serviceDefinition); - - } else { - $type = $serviceDefinition->getType(); - throw new InvalidStateException( - "It seems DI defined repository of type '$type' is not defined as one of supported DI services. - Orm can only work with ServiceDefinition or FactoryDefinition services.", + $serviceDefinitions = $this->findRepositories(); + foreach ($serviceDefinitions as $serviceName => $serviceDefinition) { + $className = $serviceDefinition->getType(); + if ($className === null) { + throw new NotSupportedException( + "Service definition $serviceName does have resolvable class type. " . + "Such service definitions are not support by Nextras Orm." + ); + } else if (!is_subclass_of($className, IRepository::class)) { + throw new InvalidArgumentException( + "Found '$className' repository is not an implementation of IRepository." ); } - - /** @var class-string> $class */ - $class = $serviceDefinition->getType(); - $repositories[$name] = $class; - $repositoriesMap[$class] = $serviceName; + $repositories[] = new DiRepositoryEntry( + className: $className, + name: $this->getRepositoryName($serviceName, $serviceDefinition), + service: $serviceDefinition, + ); } - - $this->setupRepositoryLoader($repositoriesMap); return $repositories; } - /** - * @param FactoryDefinition|ServiceDefinition|ServiceDefinition $serviceDefinition - */ - protected function getRepositoryName(string $serviceName, $serviceDefinition): string + protected function getRepositoryName(string $serviceName, ServiceDefinition $serviceDefinition): string { return $serviceName; } /** - * @param array>, string> $repositoriesMap - */ - protected function setupRepositoryLoader(array $repositoriesMap): void - { - $this->builder->addDefinition($this->extension->prefix('repositoryLoader')) - ->setType(RepositoryLoader::class) - ->setArguments([ - 'repositoryNamesMap' => $repositoriesMap, - 'extensions' => $this->extensions, - ]); - } - - - /** - * @return array + * @return array */ protected function findRepositories(): array { - return $this->builder->findByType(IRepository::class); + $services = []; + foreach ($this->builder->findByType(IRepository::class) as $service) { + $resolvedService = match (true) { + $service instanceof ServiceDefinition => $service, + $service instanceof FactoryDefinition => $service->getResultDefinition(), + default => throw new NotSupportedException("Service " . $service::class . " type is not supported by Nextras Orm.") + }; + $services[$resolvedService->getName() ?? $resolvedService->getType() ?? ""] = $resolvedService; + } + return $services; } } diff --git a/src/Bridges/NetteDI/DiRepositoryEntry.php b/src/Bridges/NetteDI/DiRepositoryEntry.php new file mode 100644 index 000000000..32fc9c17b --- /dev/null +++ b/src/Bridges/NetteDI/DiRepositoryEntry.php @@ -0,0 +1,22 @@ +> $className + */ + public function __construct( + public readonly string $className, + public readonly string|null $name, + public readonly ServiceDefinition $service, + ) + { + } +} diff --git a/src/Bridges/NetteDI/DiRepositoryLoader.php b/src/Bridges/NetteDI/DiRepositoryLoader.php new file mode 100644 index 000000000..9466ffd07 --- /dev/null +++ b/src/Bridges/NetteDI/DiRepositoryLoader.php @@ -0,0 +1,85 @@ +>, string> $repositoryClassNameToDiNameMap + * @param array $repositoryNameToDiNameMap + * @param array, class-string>> $entityClassNameToRepositoryClassNameMap + */ + public function __construct( + private readonly Container $container, + private readonly array $repositoryClassNameToDiNameMap, + private readonly array $repositoryNameToDiNameMap, + private readonly array $entityClassNameToRepositoryClassNameMap, + ) + { + } + + + #[\Override] + public function hasRepository(string $className): bool + { + return isset($this->repositoryClassNameToDiNameMap[$className]); + } + + + #[\Override] + public function hasRepositoryByName(string $name): bool + { + return isset($this->repositoryNameToDiNameMap[$name]); + } + + + #[\Override] + public function getRepository(string $className): IRepository|null + { + return $this->container->getService($this->repositoryClassNameToDiNameMap[$className]); + } + + + #[\Override] + public function getRepositoryByName(string $name): IRepository|null + { + return $this->container->getService($this->repositoryNameToDiNameMap[$name]); + } + + + /** + * @template E of IEntity + * @param class-string $entityClassName + * @return class-string>|null + */ + #[\Override] + public function getRepositoryClassNameForEntity(string $entityClassName): string|null + { + /** @var class-string>|null */ + return $this->entityClassNameToRepositoryClassNameMap[$entityClassName] ?? null; + } + + + #[\Override] + public function getInitializedRepositories(): array + { + $repositories = []; + foreach ($this->repositoryClassNameToDiNameMap as $diName) { + if ($this->container->isCreated($diName)) { + $repositories[] = $this->container->getService($diName); + } + } + return $repositories; + } +} diff --git a/src/Bridges/NetteDI/IRepositoryFinder.php b/src/Bridges/NetteDI/IRepositoryFinder.php index 1f252d7a3..be4c4d862 100644 --- a/src/Bridges/NetteDI/IRepositoryFinder.php +++ b/src/Bridges/NetteDI/IRepositoryFinder.php @@ -4,33 +4,32 @@ use Nette\DI\ContainerBuilder; -use Nette\DI\Definitions\Statement; -use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Model\IModel; -use Nextras\Orm\Repository\IRepository; interface IRepositoryFinder { /** * @param class-string $modelClass - * @param list $extensions */ - public function __construct(string $modelClass, array $extensions, ContainerBuilder $containerBuilder, OrmExtension $extension); - + public function __construct( + ContainerBuilder $builder, + OrmExtension $extension, + string $modelClass, + ); /** - * Load configuration DIC phase. - * Returns array of repositories or null if they are loaded in the other phase. - * @return array>> + * Registers repositories. + * + * The repository finder may not do anything if it reuses already registered repositories. */ - public function loadConfiguration(): ?array; - + public function registerRepositories(): void; /** - * Before compile DIC phase. - * Returns array of repositories or null if they are loaded in the other phase. - * @return array>> + * Resolves a list of repositories for the current $modelClass. + * The {@see OrmExtension} will reuse discovered service definition for final setup with the Model. + * + * @return list */ - public function beforeCompile(): ?array; + public function resolveRepositories(): array; } diff --git a/src/Bridges/NetteDI/OrmExtension.php b/src/Bridges/NetteDI/OrmExtension.php index 99c483d4e..88d486b1b 100644 --- a/src/Bridges/NetteDI/OrmExtension.php +++ b/src/Bridges/NetteDI/OrmExtension.php @@ -6,19 +6,18 @@ use Nette\Caching\Cache; use Nette\DI\CompilerExtension; use Nette\DI\ContainerBuilder; +use Nette\DI\Definitions\Reference; +use Nette\DI\Definitions\ServiceDefinition; 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\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; use Nextras\Orm\Model\Model; -use Nextras\Orm\Repository\IRepository; use stdClass; use function is_subclass_of; @@ -35,6 +34,9 @@ class OrmExtension extends CompilerExtension /** @var class-string */ protected string $modelClass; + /** @var list */ + protected array $extensions = []; + public function getConfigSchema(): Schema { @@ -54,50 +56,65 @@ 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; - } + $this->extensions = $this->setupExtensions(); $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, $extensions, $this->builder, $this); - $repositories = $this->repositoryFinder->loadConfiguration(); + $this->repositoryFinder = new $repositoryFinderClass($this->builder, $this, $this->modelClass); + $this->repositoryFinder->registerRepositories(); $this->setupCache(); $this->setupDependencyProvider(); - $this->setupDbalMapperDependencies(); - $this->setupMetadataParserFactory($extensions); - - if ($repositories !== null) { - $repositoriesConfig = Model::getConfiguration($repositories); - $this->setupMetadataStorage($repositoriesConfig[2]); - $this->setupModel($this->modelClass, $repositoriesConfig, $extensions); - } - - $this->initializeMetadata($this->config->initializeMetadata); + $this->setupMetadataParserFactory($this->extensions); + $this->setupMetadataStorage(); + $this->setupModel($this->modelClass, $this->extensions); + $this->setupRepositoryLoader(); + $this->initializeMetadata(); } public function beforeCompile(): void { - $repositories = $this->repositoryFinder->beforeCompile(); + $repositories = $this->repositoryFinder->resolveRepositories(); + $this->configureRepositories($repositories, $this->extensions); + $this->setupDbalMapperDependencies(); + } + - if ($repositories !== null) { - $extensions = []; - foreach ($this->config->extensions as $extension) { - $extensions[] = is_string($extension) ? new Statement($extension) : $extension; + /** + * Normalizes each configured extension into a reference to a single shared + * service, so extensions are instantiated once and reused across the metadata + * factory, model and every repository setup. + * + * An extension may be given as: + * - a `@name` / `@Type` string referencing an already registered service, + * - a class name string, or + * - a {@see Statement} factory; the latter two are registered as new services. + * + * @return list + */ + protected function setupExtensions(): array + { + $references = []; + foreach ($this->config->extensions as $i => $extension) { + if (is_string($extension) && str_starts_with($extension, '@')) { + $target = substr($extension, 1); + $references[] = str_contains($target, '\\') + ? Reference::fromType($target) + : new Reference($target); + continue; } - $repositoriesConfig = Model::getConfiguration($repositories); - $this->setupMetadataStorage($repositoriesConfig[2]); - $this->setupModel($this->modelClass, $repositoriesConfig, $extensions); + $name = $this->prefix('extension.' . $i); + $this->builder->addDefinition($name) + ->setFactory($extension) + ->setAutowired(false); + $references[] = new Reference($name); } - - $this->setupDbalMapperDependencies(); + return $references; } @@ -151,7 +168,7 @@ protected function setupDbalMapperDependencies(): void /** - * @param list $extensions + * @param list $extensions */ protected function setupMetadataParserFactory(array $extensions): void { @@ -167,10 +184,7 @@ protected function setupMetadataParserFactory(array $extensions): void } - /** - * @param array, class-string>> $entityClassMap - */ - protected function setupMetadataStorage(array $entityClassMap): void + protected function setupMetadataStorage(): void { $metadataName = $this->prefix('metadataStorage'); if ($this->builder->hasDefinition($metadataName)) { @@ -180,7 +194,6 @@ protected function setupMetadataStorage(array $entityClassMap): void $this->builder->addDefinition($metadataName) ->setType(MetadataStorage::class) ->setArguments([ - 'entityClassesMap' => $entityClassMap, 'cache' => $this->prefix('@cache'), 'metadataParserFactory' => $this->prefix('@metadataParserFactory'), 'repositoryLoader' => $this->prefix('@repositoryLoader'), @@ -191,14 +204,9 @@ protected function setupMetadataStorage(array $entityClassMap): void /** * @param class-string $modelClass - * @param array{ - * array>, true>, - * array>>, - * array, class-string>> - * } $repositoriesConfig - * @param list $extensions + * @param list $extensions */ - protected function setupModel(string $modelClass, array $repositoriesConfig, array $extensions): void + protected function setupModel(string $modelClass, array $extensions): void { $modelName = $this->prefix('model'); if ($this->builder->hasDefinition($modelName)) { @@ -208,7 +216,6 @@ protected function setupModel(string $modelClass, array $repositoriesConfig, arr $this->builder->addDefinition($modelName) ->setType($modelClass) ->setArguments([ - 'configuration' => $repositoriesConfig, 'repositoryLoader' => $this->prefix('@repositoryLoader'), 'metadataStorage' => $this->prefix('@metadataStorage'), ]) @@ -216,12 +223,61 @@ protected function setupModel(string $modelClass, array $repositoriesConfig, arr } - protected function initializeMetadata(bool $init): void + protected function setupRepositoryLoader(): void { - if (!$init) { - return; + $this->builder->addDefinition($this->prefix('repositoryLoader')) + ->setType(DiRepositoryLoader::class); + } + + + /** + * @param list $repositories + * @param list $extensions + */ + protected function configureRepositories( + array $repositories, + array $extensions, + ): void + { + $classNamesToDiNameMap = []; + $nameToDiNameMap = []; + $entityClassNameToRepositoryClassNameMap = []; + + foreach ($repositories as $repository) { + $repositoryServiceName = $repository->service->getName(); + $classNamesToDiNameMap[$repository->className] = $repositoryServiceName; + if ($repository->name !== null) { + $nameToDiNameMap[$repository->name] = $repositoryServiceName; + } + $entityClassNames = $repository->className::getEntityClassNames(); + foreach ($entityClassNames as $entityClassName) { + $entityClassNameToRepositoryClassNameMap[$entityClassName] = $repository->className; + } + + $repository->service->addSetup('setModel', [$this->prefix('@model')]); + $repository->service->addSetup( + 'foreach(? as $e) { $e->configureMapper($service->getMapper()); $e->configureRepository($service); }', + [$extensions] + ); + } + + $loader = $this->builder->getDefinition($this->prefix('repositoryLoader')); + if ($loader instanceof ServiceDefinition && $loader->getType() === DiRepositoryLoader::class) { + $loader->setArgument('repositoryClassNameToDiNameMap', $classNamesToDiNameMap); + $loader->setArgument('repositoryNameToDiNameMap', $nameToDiNameMap); + $loader->setArgument('entityClassNameToRepositoryClassNameMap', $entityClassNameToRepositoryClassNameMap); + } + + $metadataStorage = $this->builder->getDefinition($this->prefix('metadataStorage')); + if ($metadataStorage instanceof ServiceDefinition) { + $metadataStorage->setArgument('entityClassesMap', $entityClassNameToRepositoryClassNameMap); } + } + + protected function initializeMetadata(): void + { + if (!$this->config->initializeMetadata) return; // getMetadata() to force initialization when Nette DI's lazy proxies are enabled. $this->initialization->addBody('$this->getService(?)->getLoadedMetadata();', [ $this->prefix('metadataStorage'), diff --git a/src/Bridges/NetteDI/PhpDocRepositoryFinder.php b/src/Bridges/NetteDI/PhpDocRepositoryFinder.php index d718502f8..7e140e3a3 100644 --- a/src/Bridges/NetteDI/PhpDocRepositoryFinder.php +++ b/src/Bridges/NetteDI/PhpDocRepositoryFinder.php @@ -4,9 +4,12 @@ use Nette\DI\ContainerBuilder; +use Nette\DI\Definitions\FactoryDefinition; +use Nette\DI\Definitions\ServiceDefinition; use Nette\Utils\Reflection; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Exception\InvalidStateException; +use Nextras\Orm\Exception\NotSupportedException; use Nextras\Orm\Exception\RuntimeException; use Nextras\Orm\Model\IModel; use Nextras\Orm\Model\Model; @@ -16,41 +19,45 @@ class PhpDocRepositoryFinder implements IRepositoryFinder { + /** @var list */ + private array $repositories = []; + + + #[\Override] public function __construct( - protected readonly string $modelClass, - protected readonly array $extensions, protected readonly ContainerBuilder $builder, protected readonly OrmExtension $extension, + protected readonly string $modelClass, ) { } - public function loadConfiguration(): ?array + #[\Override] + public function registerRepositories(): void { $repositories = $this->findRepositories($this->modelClass); - $repositoriesMap = []; - - foreach ($repositories as $repositoryName => $repositoryClass) { - $this->setupMapperService($repositoryName, $repositoryClass); - $this->setupRepositoryService($repositoryName, $repositoryClass); - $repositoriesMap[$repositoryClass] = $this->extension->prefix('repositories.' . $repositoryName); + foreach ($repositories as $name => $className) { + $service = $this->setupRepositoryService($name, $className); + $this->repositories[] = new DiRepositoryEntry( + className: $className, + name: $name, + service: $service, + ); } - - $this->setupRepositoryLoader($repositoriesMap); - return $repositories; } - public function beforeCompile(): ?array + #[\Override] + public function resolveRepositories(): array { - return null; + return $this->repositories; } /** * @param class-string $modelClass - * @return array>> + * @return array>> */ protected function findRepositories(string $modelClass): array { @@ -89,6 +96,31 @@ protected function findRepositories(string $modelClass): array } + /** + * @param class-string> $repositoryClass + */ + protected function setupRepositoryService(string $repositoryName, string $repositoryClass): ServiceDefinition + { + $serviceName = $this->extension->prefix('repositories.' . $repositoryName); + if ($this->builder->hasDefinition($serviceName)) { + $service = $this->builder->getDefinition($serviceName); + return match (true) { + $service instanceof ServiceDefinition => $service, + $service instanceof FactoryDefinition => $service->getResultDefinition(), + default => throw new NotSupportedException("Service " . $service::class . " type is not supported by Nextras Orm.") + }; + } + + $this->setupMapperService($repositoryName, $repositoryClass); + return $this->builder->addDefinition($serviceName) + ->setType($repositoryClass) + ->setArguments([ + $this->extension->prefix('@mappers.' . $repositoryName), + $this->extension->prefix('@dependencyProvider'), + ]); + } + + protected function setupMapperService(string $repositoryName, string $repositoryClass): void { $mapperName = $this->extension->prefix('mappers.' . $repositoryName); @@ -98,7 +130,7 @@ protected function setupMapperService(string $repositoryName, string $repository $mapperClass = str_replace('Repository', 'Mapper', $repositoryClass); if (!class_exists($mapperClass)) { - throw new InvalidStateException("Unknown mapper for '{$repositoryName}' repository."); + throw new InvalidStateException("Unknown $mapperClass mapper for '{$repositoryName}' repository."); } /** @var \stdClass $config */ @@ -112,42 +144,8 @@ protected function setupMapperService(string $repositoryName, string $repository $this->builder->addDefinition($mapperName) ->setType($mapperClass) ->setArguments([ - 'cache' => $this->extension->prefix('@cache'), - 'mapperCoordinator' => $this->extension->prefix('@mapperCoordinator'), - ] + $connection); - } - - - /** - * @param class-string> $repositoryClass - */ - protected function setupRepositoryService(string $repositoryName, string $repositoryClass): void - { - $serviceName = $this->extension->prefix('repositories.' . $repositoryName); - if ($this->builder->hasDefinition($serviceName)) { - return; - } - - $this->builder->addDefinition($serviceName) - ->setType($repositoryClass) - ->setArguments([ - $this->extension->prefix('@mappers.' . $repositoryName), - $this->extension->prefix('@dependencyProvider'), - ]) - ->addSetup('setModel', [$this->extension->prefix('@model')]); - } - - - /** - * @param array>, string> $repositoriesMap - */ - protected function setupRepositoryLoader(array $repositoriesMap): void - { - $this->builder->addDefinition($this->extension->prefix('repositoryLoader')) - ->setType(RepositoryLoader::class) - ->setArguments([ - 'repositoryNamesMap' => $repositoriesMap, - 'extensions' => $this->extensions, - ]); + 'cache' => $this->extension->prefix('@cache'), + 'mapperCoordinator' => $this->extension->prefix('@mapperCoordinator'), + ] + $connection); } } diff --git a/src/Bridges/NetteDI/RepositoryLoader.php b/src/Bridges/NetteDI/RepositoryLoader.php deleted file mode 100644 index 311b35171..000000000 --- a/src/Bridges/NetteDI/RepositoryLoader.php +++ /dev/null @@ -1,65 +0,0 @@ - */ - 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, - ) - { - } - - - public function hasRepository(string $className): bool - { - return isset($this->repositoryNamesMap[$className]); - } - - - /** - * Returns instance of repository. - * @template R of IRepository<*> - * @param class-string $className - * @return R - */ - public function getRepository(string $className): IRepository - { - /** @var R */ - $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; - } - - - public function isCreated(string $className): bool - { - return $this->container->isCreated($this->repositoryNamesMap[$className]); - } -} diff --git a/src/Model/IModel.php b/src/Model/IModel.php index 0858c279d..0b2130a74 100644 --- a/src/Model/IModel.php +++ b/src/Model/IModel.php @@ -17,7 +17,7 @@ public function hasRepositoryByName(string $name): bool; /** * Returns repository by repository name. - * @return IRepository + * @return IRepository<*> */ public function getRepositoryByName(string $name): IRepository; diff --git a/src/Model/IRepositoryLoader.php b/src/Model/IRepositoryLoader.php index a5bf432bd..28cb3bb2e 100644 --- a/src/Model/IRepositoryLoader.php +++ b/src/Model/IRepositoryLoader.php @@ -17,18 +17,41 @@ public function hasRepository(string $className): bool; /** - * Returns instance of repository. + * Returns true if repository exists with the simple $name. + * Repository may not be registered with a name. Class name is implicitly always present, though. + */ + public function hasRepositoryByName(string $name): bool; + + /** + * Returns instance of repository or null if it does not exist or is not registered into the Model and its loader. + * * @template T of IRepository<*> * @param class-string $className - * @return T + * @return T|null + */ + public function getRepository(string $className): IRepository|null; + + /** + * Returns instance of repository or null if it does not exist or is not registered into the Model and its loader + * via the $name. + * + * @return IRepository<*>|null */ - public function getRepository(string $className): IRepository; + public function getRepositoryByName(string $name): IRepository|null; /** - * Checks, if repository has been already created. - * @template T of IRepository - * @param class-string $className + * Returns class name of entity's repository or null if no repository manages such entity. + * @template E of IEntity + * @param class-string $entityClassName + * @return class-string>|null + */ + public function getRepositoryClassNameForEntity(string $entityClassName): string|null; + + + /** + * Returns list of loaded (already initialized) repositories. + * @return list> */ - public function isCreated(string $className): bool; + public function getInitializedRepositories(): array; } diff --git a/src/Model/Model.php b/src/Model/Model.php index 689840846..8b656bc40 100644 --- a/src/Model/Model.php +++ b/src/Model/Model.php @@ -11,10 +11,8 @@ use Nextras\Orm\Repository\IRepository; use Nextras\Orm\Repository\PersistenceHelper; use Nextras\Orm\Repository\PersistenceMode; -use function array_keys; use function array_merge; use function get_class; -use function is_object; use function is_string; @@ -23,45 +21,10 @@ class Model implements IModel use SmartObject; /** @var list */ - public $onFlush = []; + public array $onFlush = []; - /** - * Creates repository list configuration. - * @param array>|IRepository<*>> $repositories - * @return array{ - * array>, true>, - * array>>, - * array, class-string>> - * } - */ - public static function getConfiguration(array $repositories): array - { - $config = [[], [], []]; - foreach ($repositories as $name => $repository) { - /** - * @var class-string> $className - */ - $className = is_object($repository) ? get_class($repository) : $repository; - $config[0][$className] = true; - $config[1][$name] = $className; - foreach ($repository::getEntityClassNames() as $entityClassName) { - $config[2][$entityClassName] = $className; - } - } - return $config; - } - - - /** - * @param array{ - * array>, true>, - * array>>, - * array, class-string>> - * } $configuration - */ public function __construct( - private readonly array $configuration, private readonly IRepositoryLoader $repositoryLoader, private readonly MetadataStorage $metadataStorage ) @@ -71,55 +34,36 @@ public function __construct( public function hasRepositoryByName(string $name): bool { - return isset($this->configuration[1][$name]); + return $this->repositoryLoader->hasRepositoryByName($name); } public function getRepositoryByName(string $name): IRepository { - if (!isset($this->configuration[1][$name])) { - throw new InvalidArgumentException("Repository '$name' does not exist."); - } - return $this->getRepository($this->configuration[1][$name]); + return $this->repositoryLoader->getRepositoryByName($name) + ?? throw new InvalidArgumentException("Repository with '$name' simple name does not exist."); } public function hasRepository(string $className): bool { - return isset($this->configuration[0][$className]); + return $this->repositoryLoader->hasRepository($className); } - /** - * Returns repository by repository class. - * @template E of IEntity - * @template T of IRepository - * @param class-string $className - * @return T - */ public function getRepository(string $className): IRepository { - if (!isset($this->configuration[0][$className])) { - throw new InvalidArgumentException("Repository '$className' does not exist."); - } - $repository = $this->repositoryLoader->getRepository($className); - return $repository; + return $this->repositoryLoader->getRepository($className) + ?? throw new InvalidArgumentException("Repository with '$className' class name does not exist."); } - /** - * @template E of IEntity - * @param E|class-string $entity - * @return IRepository - */ public function getRepositoryForEntity($entity): IRepository { $entityClassName = is_string($entity) ? $entity : get_class($entity); - if (!isset($this->configuration[2][$entityClassName])) { - throw new InvalidArgumentException("Repository for '$entityClassName' entity does not exist."); - } - /** @var IRepository */ - return $this->getRepository($this->configuration[2][$entityClassName]); + $repositoryClassName = $this->repositoryLoader->getRepositoryClassNameForEntity($entityClassName) + ?? throw new InvalidArgumentException("No repository manages '$entityClassName' entity."); + return $this->getRepository($repositoryClassName); } @@ -163,7 +107,7 @@ public function flush(): void { $allPersisted = []; $allRemoved = []; - foreach ($this->getLoadedRepositories() as $repository) { + foreach ($this->repositoryLoader->getInitializedRepositories() as $repository) { [$persisted, $removed] = $repository->doFlush(); $allPersisted = array_merge($allPersisted, $persisted); $allRemoved = array_merge($allRemoved, $removed); @@ -175,7 +119,7 @@ public function flush(): void public function clear(): void { - foreach ($this->getLoadedRepositories() as $repository) { + foreach ($this->repositoryLoader->getInitializedRepositories() as $repository) { $repository->doClear(); } } @@ -183,15 +127,15 @@ public function clear(): void public function refreshAll(bool $allowOverwrite = false): void { - foreach ($this->getLoadedRepositories() as $repository) { + foreach ($this->repositoryLoader->getInitializedRepositories() as $repository) { $repository->doRefreshAll($allowOverwrite); } } /** - * Returns repository by name. - * @return IRepository + * Returns repository by its name. + * @return IRepository<*> */ public function &__get(string $name): IRepository { @@ -205,8 +149,8 @@ protected function processPersist(PersistenceMode $mode, IEntity $entity, bool $ [$queuePersist, $queueRemove] = PersistenceHelper::getCascadeQueue($entity, $mode, $this, $withCascade); foreach ($queuePersist as $object) { if ($object instanceof IEntity) { - $repository = $this->configuration[2][get_class($object)]; - $this->repositoryLoader->getRepository($repository)->doPersist($object); + $repository = $this->getRepositoryForEntity($object); + $repository->doPersist($object); } elseif ($object instanceof IRelationshipCollection) { $object->doPersist(); } elseif ($object instanceof IRelationshipContainer) { @@ -214,24 +158,8 @@ protected function processPersist(PersistenceMode $mode, IEntity $entity, bool $ } } foreach ($queueRemove as $object) { - $repository = $this->configuration[2][get_class($object)]; - $this->repositoryLoader->getRepository($repository)->doRemove($object); + $repository = $this->getRepositoryForEntity($object); + $repository->doRemove($object); } } - - - /** - * @return list> - */ - private function getLoadedRepositories(): array - { - $repositories = []; - foreach (array_keys($this->configuration[0]) as $className) { - if ($this->repositoryLoader->isCreated($className)) { - $repositories[] = $this->repositoryLoader->getRepository($className); - } - } - - return $repositories; - } } diff --git a/src/Model/SimpleModelFactory.php b/src/Model/SimpleModelFactory.php index 743922616..78730a110 100644 --- a/src/Model/SimpleModelFactory.php +++ b/src/Model/SimpleModelFactory.php @@ -9,14 +9,13 @@ use Nextras\Orm\Entity\Reflection\MetadataParserFactory; use Nextras\Orm\Extension; use Nextras\Orm\Repository\IRepository; -use function array_values; +use function get_class; class SimpleModelFactory { /** - * @template E of IEntity - * @param array> $repositories Map of a repository name and its instance. The name is used + * @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 */ @@ -32,11 +31,11 @@ public function __construct( public function create(): Model { - $config = Model::getConfiguration($this->repositories); + $entityClassesMap = $this->getEntityClassesMap(); $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); + $loader = new SimpleRepositoryLoader($this->repositories, $entityClassesMap); + $metadata = new MetadataStorage($entityClassesMap, $this->cache, $parser, $loader); + $model = new Model($loader, $metadata); foreach ($this->repositories as $repository) { $repository->setModel($model); @@ -52,4 +51,20 @@ public function create(): Model return $model; } + + + /** + * @return array, class-string>> + */ + private function getEntityClassesMap(): array + { + $map = []; + foreach ($this->repositories as $repository) { + foreach ($repository::getEntityClassNames() as $entityClassName) { + $map[$entityClassName] = get_class($repository); + } + } + /** @var array, class-string>> $map */ + return $map; + } } diff --git a/src/Model/SimpleRepositoryLoader.php b/src/Model/SimpleRepositoryLoader.php index d7dfc659b..b5fd5caa7 100644 --- a/src/Model/SimpleRepositoryLoader.php +++ b/src/Model/SimpleRepositoryLoader.php @@ -4,51 +4,94 @@ use Nextras\Orm\Entity\IEntity; -use Nextras\Orm\Exception\InvalidArgumentException; use Nextras\Orm\Repository\IRepository; +use function array_values; +use function get_class; +/** + * Loads repositories from a static list of already instantiated repositories. + * All repositories are considered initialized right away. + */ class SimpleRepositoryLoader implements IRepositoryLoader { /** @var array>, IRepository<*>> */ private array $repositories = []; + /** @var array>> */ + private array $nameToClassNameMap = []; + /** - * @param list> $repositories + * @param array> $repositories Map of a repository simple name and its instance. The name + * is used for accessing the repository by name in contrast to accessing it by its class-string. + * @param array, class-string>> $entityClassNameToClassNameMap Map of an + * entity class name to the class name of the repository that manages it. */ - public function __construct(array $repositories) + public function __construct( + array $repositories, + private readonly array $entityClassNameToClassNameMap = [], + ) { - foreach ($repositories as $repository) { - $this->repositories[get_class($repository)] = $repository; + foreach ($repositories as $name => $repository) { + $className = get_class($repository); + $this->repositories[$className] = $repository; + $this->nameToClassNameMap[$name] = $className; } } + #[\Override] public function hasRepository(string $className): bool { return isset($this->repositories[$className]); } + #[\Override] + public function hasRepositoryByName(string $name): bool + { + return isset($this->nameToClassNameMap[$name]); + } + + /** - * Returns instance of repository. * @template T of IRepository<*> * @param class-string $className - * @return T + * @return T|null */ - public function getRepository(string $className): IRepository + #[\Override] + public function getRepository(string $className): IRepository|null { - if (!isset($this->repositories[$className])) { - throw new InvalidArgumentException("Repository '$className' not defined."); - } - /** @var T */ - return $this->repositories[$className]; + /** @var T|null */ + return $this->repositories[$className] ?? null; + } + + + #[\Override] + public function getRepositoryByName(string $name): IRepository|null + { + $className = $this->nameToClassNameMap[$name] ?? null; + return $className !== null ? $this->repositories[$className] : null; + } + + + /** + * @template E of IEntity + * @param class-string $entityClassName + * @return class-string>|null + */ + #[\Override] + public function getRepositoryClassNameForEntity(string $entityClassName): string|null + { + /** @var class-string>|null */ + return $this->entityClassNameToClassNameMap[$entityClassName] ?? null; } - public function isCreated(string $className): bool + #[\Override] + public function getInitializedRepositories(): array { - return true; + return array_values($this->repositories); } } diff --git a/tests/cases/integration/BridgeNetteDI/dic-extension-multiple.neon b/tests/cases/integration/BridgeNetteDI/dic-extension-multiple.neon index 3b53711c6..af40792d7 100644 --- a/tests/cases/integration/BridgeNetteDI/dic-extension-multiple.neon +++ b/tests/cases/integration/BridgeNetteDI/dic-extension-multiple.neon @@ -6,11 +6,11 @@ extensions: nette.cache: Nette\Bridges\CacheDI\CacheExtension(%tempDir%) nextras.orm1: - model: NextrasTests\Orm\Model + model: NextrasTests\Orm\TestAppModel connection: @nextras.dbal1.connection nextras.orm2: - model: NextrasTests\Orm\Integration\BridgeNetteDI\Model2 + model: NextrasTests\Orm\Integration\BridgeNetteDI\TestAppModel2 connection: @nextras.dbal2.connection autowiredInternalServices: false diff --git a/tests/cases/integration/BridgeNetteDI/dic-extension-multiple.phpt b/tests/cases/integration/BridgeNetteDI/dic-extension-multiple.phpt index e130334ec..8b7a1759f 100644 --- a/tests/cases/integration/BridgeNetteDI/dic-extension-multiple.phpt +++ b/tests/cases/integration/BridgeNetteDI/dic-extension-multiple.phpt @@ -64,7 +64,7 @@ class TestMapper extends DbalMapper /** * @property-read TestRepository $test */ -class Model2 extends Model +class TestAppModel2 extends Model { } diff --git a/tests/cases/integration/BridgeNetteDI/dic-finder.neon b/tests/cases/integration/BridgeNetteDI/dic-finder.neon index fc9ce09ba..c5d5fe257 100644 --- a/tests/cases/integration/BridgeNetteDI/dic-finder.neon +++ b/tests/cases/integration/BridgeNetteDI/dic-finder.neon @@ -3,7 +3,7 @@ extensions: nette.cache: Nette\Bridges\CacheDI\CacheExtension(%tempDir%) nextras.orm: - model: NextrasTests\Orm\Model + model: NextrasTests\Orm\TestAppModel repositoryFinder: Nextras\Orm\Bridges\NetteDI\DIRepositoryFinder services: diff --git a/tests/cases/integration/Mapper/file.general.phpt b/tests/cases/integration/Mapper/file.general.phpt index c9c81022c..24cfa0ee6 100644 --- a/tests/cases/integration/Mapper/file.general.phpt +++ b/tests/cases/integration/Mapper/file.general.phpt @@ -18,7 +18,7 @@ use NextrasTests\Orm\Book; use NextrasTests\Orm\BooksRepository; use NextrasTests\Orm\ContentsRepository; use NextrasTests\Orm\EansRepository; -use NextrasTests\Orm\Model; +use NextrasTests\Orm\TestAppModel; use NextrasTests\Orm\Publisher; use NextrasTests\Orm\PublishersRepository; use NextrasTests\Orm\Tag; @@ -35,7 +35,7 @@ class FileMapperTest extends TestCase { public function testGeneral(): void { - /** @var Model $orm */ + /** @var TestAppModel $orm */ $orm = $this->createOrm(); $author = new Author(); @@ -61,7 +61,7 @@ class FileMapperTest extends TestCase $orm->authors->persistAndFlush($author); - /** @var Model $orm */ + /** @var TestAppModel $orm */ $orm = $this->createOrm(); $book3 = new Book(); $book3->author = $orm->authors->getByIdChecked(1); @@ -71,7 +71,7 @@ class FileMapperTest extends TestCase $orm->books->persistAndFlush($book3); - /** @var Model $orm */ + /** @var TestAppModel $orm */ $orm = $this->createOrm(); /** @var Author $author */ $author = $orm->authors->findAll()->fetch(); diff --git a/tests/config.array.neon b/tests/config.array.neon index c001b55a0..397254b5a 100644 --- a/tests/config.array.neon +++ b/tests/config.array.neon @@ -2,7 +2,7 @@ extensions: nextras.orm: Nextras\Orm\Bridges\NetteDI\OrmExtension nextras.orm: - model: NextrasTests\Orm\Model + model: NextrasTests\Orm\TestAppModel repositoryFinder: Nextras\Orm\Bridges\NetteDI\TestMapperPhpDocRepositoryFinder extensions: - @NextrasTests\Orm\TestExtension diff --git a/tests/config.dbal.neon b/tests/config.dbal.neon index 4221c8ce1..597126786 100644 --- a/tests/config.dbal.neon +++ b/tests/config.dbal.neon @@ -24,7 +24,7 @@ nextras.dbal2: TrustServerCertificate: true nextras.orm: - model: NextrasTests\Orm\Model + model: NextrasTests\Orm\TestAppModel services: - Nextras\Orm\TestHelper\EntityCreator diff --git a/tests/db/array-data.php b/tests/db/array-data.php index e941d70bc..8b0636d51 100644 --- a/tests/db/array-data.php +++ b/tests/db/array-data.php @@ -7,7 +7,7 @@ use Nextras\Dbal\Utils\DateTimeImmutable; -/** @var Model $orm */ +/** @var TestAppModel $orm */ $author1 = new Author(); $author1->name = 'Writer 1'; diff --git a/tests/db/array-init.php b/tests/db/array-init.php index d77079f6a..f70b33b6e 100644 --- a/tests/db/array-init.php +++ b/tests/db/array-init.php @@ -6,7 +6,7 @@ use Nextras\Orm\Collection\ArrayCollection; -/** @var Model $orm */ +/** @var TestAppModel $orm */ $orm->books->getMapper()->addMethod('findBooksWithEvenId', function () use ($orm) { $books = []; diff --git a/tests/inc/TestCase.php b/tests/inc/TestCase.php index 32087ffe8..f50b5d4f5 100644 --- a/tests/inc/TestCase.php +++ b/tests/inc/TestCase.php @@ -23,7 +23,7 @@ class TestCase extends Tester\TestCase /** @var Container */ protected $container; - /** @var Model */ + /** @var TestAppModel */ protected $orm; /** @var string|null */ @@ -78,7 +78,7 @@ protected function setUp() $configurator->setTempDirectory(TEMP_DIR); $this->container = $configurator->createContainer(); - $this->orm = $this->container->getByType(Model::class); + $this->orm = $this->container->getByType(TestAppModel::class); if ($this->section === Helper::SECTION_ARRAY) { $orm = $this->orm; diff --git a/tests/inc/model/Model.php b/tests/inc/model/TestAppModel.php similarity index 91% rename from tests/inc/model/Model.php rename to tests/inc/model/TestAppModel.php index fc548a509..1ef0900ea 100644 --- a/tests/inc/model/Model.php +++ b/tests/inc/model/TestAppModel.php @@ -3,7 +3,7 @@ namespace NextrasTests\Orm; -use Nextras\Orm\Model\Model as OrmModel; +use Nextras\Orm\Model\Model; /** @@ -24,6 +24,6 @@ * @property-read UserStatsXRepository $userStatsX * @property-read TimeSeriesRepository $timeSeries */ -class Model extends OrmModel +class TestAppModel extends Model { }