From 57c7a07b27262f71984014f3febeca02b0937b33 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 14:39:17 +0000 Subject: [PATCH] Docs: Add comprehensive documentation for missing features Document previously undocumented features: - ManagerRegistry::reopen() static method for reopening closed EntityManagers - resolveTargetEntities configuration for interface-based entity architecture - Custom DQL functions (string, numeric, datetime) with examples - Custom hydration modes with implementation example - Filters configuration with enable/disable and runtime management - Events system with auto-discovered event subscribers - Customization options: naming strategy, quote strategy, repository factory, entity listener resolver - Default query hints configuration - Multiple connections and managers setup with usage examples Update table of contents to include all new sections. --- .docs/README.md | 536 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 536 insertions(+) diff --git a/.docs/README.md b/.docs/README.md index 1581b0cc6..fdbcb7984 100755 --- a/.docs/README.md +++ b/.docs/README.md @@ -15,6 +15,18 @@ Integration of [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html - [Attributes](#attributes) - [XML](#xml) - [Helper](#helper) + - [Resolve Target Entities](#resolve-target-entities) + - [Custom DQL Functions](#custom-dql-functions) + - [Custom Hydration Modes](#custom-hydration-modes) + - [Filters](#filters) + - [Events](#events) + - [Customization](#customization) + - [Naming Strategy](#naming-strategy) + - [Quote Strategy](#quote-strategy) + - [Repository Factory](#repository-factory) + - [Entity Listener Resolver](#entity-listener-resolver) + - [Default Query Hints](#default-query-hints) + - [Multiple Connections](#multiple-connections) - [DBAL](#dbal) - [Console](#console) - [Static analyses](#static-analyses) @@ -72,6 +84,7 @@ nettrine.orm: proxyNamespace: metadataDriverImpl: entityNamespaces: + resolveTargetEntities: customStringFunctions: customNumericFunctions: customDatetimeFunctions: @@ -199,6 +212,24 @@ $managerRegistry->resetManager('second'); > you have to reset the current one using internal methods (reflection & binding). > Class responsible for this operation is [`Nettrine\ORM\ManagerRegistry`](https://github.com/contributte/doctrine-orm/blob/master/src/ManagerRegistry.php). +**Reopen (Static Method)** + +If you need to reopen an EntityManager without resetting it (keeping the same instance), you can use the static `reopen` method directly. +This is useful when you have a reference to a closed EntityManager and want to reopen it without going through the registry. + +```php +use Nettrine\ORM\ManagerRegistry; + +// Reopen a closed EntityManager directly +ManagerRegistry::reopen($entityManager); + +// Also works with EntityManagerDecorator +ManagerRegistry::reopen($decoratedEntityManager); +``` + +This method uses internal binding to access the private `$closed` property of the EntityManager and sets it to `false`. +It's particularly useful in testing scenarios or when you need to recover from an exception that closed the EntityManager. + ### Caching > [!TIP] @@ -417,6 +448,511 @@ extensions: category: App\Model\DI\DoctrineMappingExtension ``` +### Resolve Target Entities + +The `resolveTargetEntities` configuration allows you to map interfaces or abstract classes to concrete entity implementations. +This is useful for creating reusable modules that depend on entity interfaces rather than concrete implementations. + +> [!TIP] +> Take a look at more information in official Doctrine documentation: +> - https://www.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/resolve-target-entity-listener.html + +```neon +nettrine.orm: + managers: + default: + connection: default + resolveTargetEntities: + App\Model\UserInterface: App\Database\Entity\User + App\Model\OrderInterface: App\Database\Entity\Order + mapping: + App: + directories: [%appDir%/Database] + namespace: App\Database +``` + +Example usage in entity: + +```php + [!TIP] +> Take a look at more information in official Doctrine documentation: +> - https://www.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/dql-user-defined-functions.html + +```neon +nettrine.orm: + managers: + default: + connection: default + customStringFunctions: + SOUNDEX: App\Doctrine\Functions\SoundexFunction + GROUP_CONCAT: App\Doctrine\Functions\GroupConcatFunction + customNumericFunctions: + FLOOR: App\Doctrine\Functions\FloorFunction + ROUND: App\Doctrine\Functions\RoundFunction + customDatetimeFunctions: + DATE_FORMAT: App\Doctrine\Functions\DateFormatFunction + DATEDIFF: App\Doctrine\Functions\DateDiffFunction + mapping: + App: + directories: [%appDir%/Database] + namespace: App\Database +``` + +Example custom function implementation: + +```php +stringExpression->dispatch($sqlWalker) . ')'; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $this->stringExpression = $parser->StringPrimary(); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} +``` + +### Custom Hydration Modes + +Custom hydration modes allow you to define how query results are transformed into PHP objects or arrays. + +> [!TIP] +> Take a look at more information in official Doctrine documentation: +> - https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#custom-hydration-modes + +```neon +nettrine.orm: + managers: + default: + connection: default + customHydrationModes: + CustomArrayMode: App\Doctrine\Hydrators\CustomArrayHydrator + mapping: + App: + directories: [%appDir%/Database] + namespace: App\Database +``` + +Example custom hydrator: + +```php +statement()->fetchAssociative()) { + $result[] = $this->processRow($row); + } + return $result; + } + + private function processRow(array $row): array + { + // Custom transformation logic + return $row; + } +} +``` + +Usage: + +```php +$query = $entityManager->createQuery('SELECT u FROM App\Entity\User u'); +$results = $query->getResult('CustomArrayMode'); +``` + +### Filters + +Filters provide a way to add SQL conditions to all queries for specific entities. +This is useful for implementing soft deletes, multi-tenancy, or other global query constraints. + +> [!TIP] +> Take a look at more information in official Doctrine documentation: +> - https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/filters.html + +```neon +nettrine.orm: + managers: + default: + connection: default + filters: + softDelete: + class: App\Doctrine\Filters\SoftDeleteFilter + enabled: true + tenant: + class: App\Doctrine\Filters\TenantFilter + enabled: false + mapping: + App: + directories: [%appDir%/Database] + namespace: App\Database +``` + +- `class` - The filter class that extends `Doctrine\ORM\Query\Filter\SQLFilter` +- `enabled` - Whether the filter is enabled by default (optional, defaults to `false`) + +Example filter implementation: + +```php +hasField('deletedAt')) { + return ''; + } + + return sprintf('%s.deleted_at IS NULL', $targetTableAlias); + } +} +``` + +Managing filters at runtime: + +```php +$filters = $entityManager->getFilters(); + +// Enable a filter +$filters->enable('tenant'); +$filter = $filters->getFilter('tenant'); +$filter->setParameter('tenantId', $currentTenantId); + +// Disable a filter +$filters->disable('softDelete'); + +// Check if filter is enabled +$isEnabled = $filters->isEnabled('softDelete'); +``` + +### Events + +Doctrine ORM provides an event system that allows you to hook into the persistence lifecycle. +Event subscribers are automatically discovered and registered from the DI container. + +> [!TIP] +> Take a look at more information in official Doctrine documentation: +> - https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html + +**Event Subscribers** + +Simply register a service implementing `Doctrine\Common\EventSubscriber` and it will be automatically discovered: + +```neon +services: + - App\Doctrine\Subscribers\TimestampSubscriber + - App\Doctrine\Subscribers\AuditSubscriber +``` + +Example event subscriber: + +```php +getObject(); + + if (method_exists($entity, 'setCreatedAt')) { + $entity->setCreatedAt(new \DateTimeImmutable()); + } + } + + public function preUpdate(PreUpdateEventArgs $args): void + { + $entity = $args->getObject(); + + if (method_exists($entity, 'setUpdatedAt')) { + $entity->setUpdatedAt(new \DateTimeImmutable()); + } + } +} +``` + +**Lazy Event Loading** + +The `ContainerEventManager` supports lazy-loading of event listeners from the DI container. +Listeners are only instantiated when the event is actually dispatched, improving performance. + +### Customization + +#### Naming Strategy + +The naming strategy determines how entity class names and property names are converted to database table and column names. + +```neon +nettrine.orm: + managers: + default: + connection: default + namingStrategy: Doctrine\ORM\Mapping\UnderscoreNamingStrategy + mapping: + App: + directories: [%appDir%/Database] + namespace: App\Database +``` + +Available built-in strategies: +- `Doctrine\ORM\Mapping\DefaultNamingStrategy` - Uses entity/property names as-is +- `Doctrine\ORM\Mapping\UnderscoreNamingStrategy` - Converts CamelCase to snake_case (default) + +You can also use a service reference: + +```neon +services: + - App\Doctrine\CustomNamingStrategy + +nettrine.orm: + managers: + default: + namingStrategy: @App\Doctrine\CustomNamingStrategy +``` + +#### Quote Strategy + +The quote strategy determines how database identifiers (table names, column names) are quoted. + +```neon +nettrine.orm: + managers: + default: + connection: default + quoteStrategy: Doctrine\ORM\Mapping\DefaultQuoteStrategy + mapping: + App: + directories: [%appDir%/Database] + namespace: App\Database +``` + +#### Repository Factory + +The repository factory creates repository instances. You can provide a custom factory to add dependency injection to your repositories. + +```neon +nettrine.orm: + managers: + default: + connection: default + repositoryFactory: App\Doctrine\ContainerRepositoryFactory + mapping: + App: + directories: [%appDir%/Database] + namespace: App\Database +``` + +Example custom repository factory with DI container support: + +```php +container = $container; + } + + public function getRepository(EntityManagerInterface $entityManager, string $entityName): ObjectRepository + { + $repositoryHash = $entityManager->getClassMetadata($entityName)->getName() . spl_object_hash($entityManager); + + if (!isset($this->repositoryList[$repositoryHash])) { + $this->repositoryList[$repositoryHash] = $this->createRepository($entityManager, $entityName); + } + + return $this->repositoryList[$repositoryHash]; + } + + private function createRepository(EntityManagerInterface $entityManager, string $entityName): ObjectRepository + { + $metadata = $entityManager->getClassMetadata($entityName); + $repositoryClassName = $metadata->customRepositoryClassName + ?? $entityManager->getConfiguration()->getDefaultRepositoryClassName(); + + // Try to get from container first (for DI support) + $type = $this->container->getByType($repositoryClassName, false); + if ($type !== null) { + return $type; + } + + return new $repositoryClassName($entityManager, $metadata); + } +} +``` + +#### Entity Listener Resolver + +The entity listener resolver is responsible for instantiating entity listener classes. +This is useful when your entity listeners have dependencies that need to be injected. + +```neon +nettrine.orm: + managers: + default: + connection: default + entityListenerResolver: App\Doctrine\ContainerEntityListenerResolver + mapping: + App: + directories: [%appDir%/Database] + namespace: App\Database +``` + +> [!NOTE] +> By default, `Nettrine\ORM\Mapping\ContainerEntityListenerResolver` is used, which supports lazy-loading listeners from the DI container. + +### Default Query Hints + +You can configure default hints that will be applied to all queries. + +```neon +nettrine.orm: + managers: + default: + connection: default + defaultQueryHints: + doctrine.customOutputWalker: App\Doctrine\Walkers\CustomOutputWalker + mapping: + App: + directories: [%appDir%/Database] + namespace: App\Database +``` + +### Multiple Connections + +You can configure multiple database connections and entity managers for different databases or schemas. + +```neon +nettrine.dbal: + connections: + default: + driver: pdo_pgsql + host: localhost + dbname: main_db + user: root + password: secret + + analytics: + driver: pdo_mysql + host: analytics.example.com + dbname: analytics_db + user: analytics + password: secret + +nettrine.orm: + managers: + default: + connection: default + mapping: + App: + directories: [%appDir%/Database/Main] + namespace: App\Database\Main + + analytics: + connection: analytics + mapping: + Analytics: + directories: [%appDir%/Database/Analytics] + namespace: App\Database\Analytics +``` + +Using multiple managers: + +```php +// Get the default manager +$defaultManager = $managerRegistry->getManager(); +$defaultManager = $managerRegistry->getManager('default'); + +// Get a specific manager +$analyticsManager = $managerRegistry->getManager('analytics'); + +// Get a repository from a specific manager +$repository = $managerRegistry->getRepository(AnalyticsEvent::class); + +// Get all managers +$managers = $managerRegistry->getManagers(); + +// Get manager for a specific entity class +$manager = $managerRegistry->getManagerForClass(User::class); +``` + ### DBAL > [!TIP]