├── ci ├── dummy-import.sql ├── Entity │ └── Entity.php ├── config │ ├── application.config.php │ └── ci.config.php ├── run-cli-migrations.sh ├── run-cli.sh └── Version20120714005702.php ├── docs ├── images │ ├── laminas-developer-tools-doctrine-module.png │ └── laminas-developer-tools-multiple-entity-managers.png └── en │ ├── sidebar.rst │ ├── index.rst │ ├── migrations.rst │ ├── forms.rst │ ├── miscellaneous.rst │ ├── cache.rst │ ├── user-guide.rst │ ├── developer-tools.rst │ └── configuration.rst ├── src ├── Form │ ├── Element │ │ ├── EntityRadio.php │ │ ├── EntitySelect.php │ │ └── EntityMultiCheckbox.php │ └── Annotation │ │ ├── EntityBasedFormBuilder.php │ │ └── DoctrineAnnotationListener.php ├── ConfigProvider.php ├── Service │ ├── CliConfiguratorFactory.php │ ├── DoctrineObjectHydratorFactory.php │ ├── RunSqlCommandFactory.php │ ├── ReservedWordsCommandFactory.php │ ├── ObjectRadioFactory.php │ ├── ObjectSelectFactory.php │ ├── ObjectMultiCheckboxFactory.php │ ├── EntityManagerAliasCompatFactory.php │ ├── MappingCollectorFactory.php │ ├── EntityManagerFactory.php │ ├── EntityResolverFactory.php │ ├── SQLLoggerCollectorFactory.php │ ├── DBALConnectionFactory.php │ ├── DBALConfigurationFactory.php │ ├── MigrationsCommandFactory.php │ └── ConfigurationFactory.php ├── Yuml │ ├── YumlControllerFactory.php │ ├── YumlController.php │ └── MetadataGrapher.php ├── Collector │ ├── SQLLoggerCollector.php │ └── MappingCollector.php ├── Options │ ├── DBALConfiguration.php │ ├── SQLLoggerCollectorOptions.php │ ├── EntityResolver.php │ ├── EntityManager.php │ ├── SecondLevelCacheConfiguration.php │ ├── DBALConnection.php │ └── Configuration.php ├── Paginator │ └── Adapter │ │ └── DoctrinePaginator.php ├── Module.php └── CliConfigurator.php ├── Module.php ├── config ├── controllers.config.php └── module.config.php ├── LICENSE.md ├── README.md ├── .doctrine-project.json ├── composer.json └── view └── laminas-developer-tools └── toolbar ├── doctrine-orm-mappings.phtml └── doctrine-orm-queries.phtml /ci/dummy-import.sql: -------------------------------------------------------------------------------- 1 | SELECT 1; 2 | -------------------------------------------------------------------------------- /docs/images/laminas-developer-tools-doctrine-module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doctrine/DoctrineORMModule/HEAD/docs/images/laminas-developer-tools-doctrine-module.png -------------------------------------------------------------------------------- /docs/images/laminas-developer-tools-multiple-entity-managers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doctrine/DoctrineORMModule/HEAD/docs/images/laminas-developer-tools-multiple-entity-managers.png -------------------------------------------------------------------------------- /src/Form/Element/EntityRadio.php: -------------------------------------------------------------------------------- 1 | [ 10 | // Yuml controller, used to generate Yuml graphs since 11 | // yuml.me doesn't do redirects on its own 12 | YumlController::class => YumlControllerFactory::class, 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | get('doctrine.entitymanager.orm_default')); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ci/config/application.config.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'Laminas\Cache', 6 | 'Laminas\Cache\Storage\Adapter\Filesystem', 7 | 'Laminas\Cache\Storage\Adapter\Memory', 8 | 'Laminas\Form', 9 | 'Laminas\Hydrator', 10 | 'Laminas\Paginator', 11 | 'Laminas\Router', 12 | 'Laminas\Validator', 13 | 'DoctrineModule', 14 | 'DoctrineORMModule', 15 | ], 16 | 'module_listener_options' => [ 17 | 'module_paths' => [], 18 | 'config_glob_paths' => [ 19 | __DIR__ . '/module.config.php', 20 | __DIR__ . '/../ci/config/ci.config.php', 21 | ], 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /ci/run-cli-migrations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | ./vendor/bin/doctrine-module migrations:generate 4 | ./vendor/bin/doctrine-module migrations:diff -n 5 | ./vendor/bin/doctrine-module migrations:execute --up 'CiDoctrineMigrations\Version20120714005702' -n 6 | ./vendor/bin/doctrine-module migrations:migrate -n 7 | ./vendor/bin/doctrine-module migrations:sync-metadata-storage 8 | ./vendor/bin/doctrine-module migrations:list 9 | ./vendor/bin/doctrine-module migrations:current 10 | ./vendor/bin/doctrine-module migrations:latest 11 | ./vendor/bin/doctrine-module migrations:up-to-date 12 | ./vendor/bin/doctrine-module migrations:status --no-interaction 13 | ./vendor/bin/doctrine-module migrations:version --delete 'CiDoctrineMigrations\Version20120714005702' -n 14 | -------------------------------------------------------------------------------- /src/Service/RunSqlCommandFactory.php: -------------------------------------------------------------------------------- 1 | get('doctrine.connection.orm_default')), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Service/ReservedWordsCommandFactory.php: -------------------------------------------------------------------------------- 1 | get('doctrine.connection.orm_default')), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Service/ObjectRadioFactory.php: -------------------------------------------------------------------------------- 1 | get(EntityManager::class); 27 | $element = new ObjectRadio(); 28 | 29 | $element->getProxy()->setObjectManager($entityManager); 30 | 31 | return $element; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Service/ObjectSelectFactory.php: -------------------------------------------------------------------------------- 1 | get(EntityManager::class); 27 | $element = new ObjectSelect(); 28 | 29 | $element->getProxy()->setObjectManager($entityManager); 30 | 31 | return $element; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ci/run-cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | mysql -h 127.0.0.1 -u root --password='' database < ci/dummy-import.sql 4 | ./vendor/bin/doctrine-module dbal:run-sql "SELECT 1" 5 | ./vendor/bin/doctrine-module orm:clear-cache:metadata 6 | ./vendor/bin/doctrine-module orm:clear-cache:query 7 | ./vendor/bin/doctrine-module orm:clear-cache:result 8 | ./vendor/bin/doctrine-module orm:generate-proxies 9 | ./vendor/bin/doctrine-module orm:ensure-production-settings 10 | ./vendor/bin/doctrine-module orm:info 11 | ./vendor/bin/doctrine-module orm:schema-tool:create 12 | ./vendor/bin/doctrine-module orm:schema-tool:update 13 | ./vendor/bin/doctrine-module orm:validate-schema 14 | ./vendor/bin/doctrine-module dbal:run-sql "SELECT COUNT(a.id) FROM entity a" 15 | ./vendor/bin/doctrine-module orm:run-dql "SELECT COUNT(a) FROM DoctrineORMModule\Ci\Entity\Entity a" 16 | ./vendor/bin/doctrine-module orm:schema-tool:drop --dump-sql 17 | ./vendor/bin/doctrine-module orm:schema-tool:drop --force 18 | -------------------------------------------------------------------------------- /src/Service/ObjectMultiCheckboxFactory.php: -------------------------------------------------------------------------------- 1 | get(EntityManager::class); 27 | $element = new ObjectMultiCheckbox(); 28 | 29 | $element->getProxy()->setObjectManager($entityManager); 30 | 31 | return $element; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Service/EntityManagerAliasCompatFactory.php: -------------------------------------------------------------------------------- 1 | get('doctrine.entitymanager.orm_default'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2025 Doctrine Project 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 6 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 9 | Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /src/Service/MappingCollectorFactory.php: -------------------------------------------------------------------------------- 1 | getName(); 27 | $objectManager = $container->get('doctrine.entitymanager.' . $name); 28 | 29 | return new MappingCollector($objectManager->getMetadataFactory(), 'doctrine.mapping_collector.' . $name); 30 | } 31 | 32 | public function getOptionsClass(): string 33 | { 34 | throw new BadMethodCallException(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Yuml/YumlControllerFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 27 | 28 | if ( 29 | ! isset($config['laminas-developer-tools']['toolbar']['enabled']) 30 | || ! $config['laminas-developer-tools']['toolbar']['enabled'] 31 | ) { 32 | throw new ServiceNotFoundException( 33 | sprintf('Service %s could not be found', YumlController::class), 34 | ); 35 | } 36 | 37 | return new YumlController( 38 | new Client('https://yuml.me/diagram/plain/class/', ['timeout' => 30]), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Yuml/YumlController.php: -------------------------------------------------------------------------------- 1 | getRequest(); 32 | assert($request instanceof Request); 33 | $this->httpClient->setMethod(Request::METHOD_POST); 34 | $this->httpClient->setParameterPost(['dsl_text' => $request->getPost('dsl_text')]); 35 | $response = $this->httpClient->send(); 36 | 37 | if (! $response->isSuccess()) { 38 | throw new UnexpectedValueException('HTTP Request failed'); 39 | } 40 | 41 | $redirect = $this->plugin('redirect'); 42 | 43 | return $redirect->toUrl('https://yuml.me/' . $response->getBody()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Service/EntityManagerFactory.php: -------------------------------------------------------------------------------- 1 | getOptions($container, 'entitymanager'); 26 | assert($options instanceof DoctrineORMModuleEntityManager); 27 | $connection = $container->get($options->getConnection()); 28 | $config = $container->get($options->getConfiguration()); 29 | 30 | // initializing the resolver 31 | // @todo should actually attach it to a fetched event manager here, and not 32 | // rely on its factory code 33 | $container->get($options->getEntityResolver()); 34 | 35 | return EntityManager::create($connection, $config); 36 | } 37 | 38 | public function getOptionsClass(): string 39 | { 40 | return DoctrineORMModuleEntityManager::class; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/en/index.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | The DoctrineORMModule leverages `DoctrineModule `__ 5 | and integrates `Doctrine ORM `__ 6 | with `Laminas `__ quickly and easily. The following features are intended 7 | to work out of the box: 8 | 9 | - Doctrine ORM support 10 | - Multiple ORM entity managers 11 | - Multiple DBAL connections 12 | - Reuse existing PDO connections in DBAL connection 13 | 14 | Installation 15 | ------------ 16 | 17 | Run the following to install this library using `Composer `__: 18 | 19 | .. code:: bash 20 | 21 | $ composer require doctrine/doctrine-orm-module 22 | 23 | Next Steps 24 | ---------- 25 | 26 | - :doc:`User Guide `: 27 | general introduction. 28 | - :doc:`Developer Tools `: 29 | setting up Laminas Developer Tools. 30 | - :doc:`Configuration `: 31 | learn how to configure DoctrineORMModule. 32 | - :doc:`Caching `: 33 | learn how to configure caches. 34 | - :doc:`Migrations `: 35 | learn how to use database migrations. 36 | - :doc:`Laminas Forms `: 37 | learn about custom Laminas form elements. 38 | - :doc:`Laminas Forms `: 39 | learn about authentication and custom DBAL types. 40 | -------------------------------------------------------------------------------- /src/Service/EntityResolverFactory.php: -------------------------------------------------------------------------------- 1 | getOptions($container, 'entity_resolver'); 22 | assert($options instanceof EntityResolver); 23 | $eventManager = $container->get($options->getEventManager()); 24 | $resolvers = $options->getResolvers(); 25 | 26 | $targetEntityListener = new ResolveTargetEntityListener(); 27 | 28 | foreach ($resolvers as $oldEntity => $newEntity) { 29 | $targetEntityListener->addResolveTargetEntity($oldEntity, $newEntity, []); 30 | } 31 | 32 | $eventManager->addEventSubscriber($targetEntityListener); 33 | 34 | return $eventManager; 35 | } 36 | 37 | /** 38 | * Get the class name of the options associated with this factory. 39 | */ 40 | public function getOptionsClass(): string 41 | { 42 | return EntityResolver::class; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Doctrine ORM Module for Laminas 2 | =============================== 3 | 4 | [![Build Status](https://github.com/doctrine/DoctrineORMModule/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/doctrine/DoctrineORMModule/actions/workflows/continuous-integration.yml) 5 | [![Code Coverage](https://codecov.io/gh/doctrine/DoctrineORMModule/graphs/badge.svg)](https://app.codecov.io/gh/doctrine/DoctrineORMModule) 6 | [![Latest Stable Version](https://poser.pugx.org/doctrine/doctrine-orm-module/v)](https://packagist.org/packages/doctrine/doctrine-orm-module) 7 | [![Total Downloads](https://poser.pugx.org/doctrine/doctrine-orm-module/downloads)](https://packagist.org/packages/doctrine/doctrine-orm-module) 8 | 9 | The DoctrineORMModule leverages [DoctrineModule](https://github.com/doctrine/DoctrineModule/) and integrates 10 | [Doctrine ORM](https://github.com/doctrine/orm) with [Laminas](https://getlaminas.org/) quickly 11 | and easily. The following features are intended to work out of the box: 12 | 13 | - Doctrine ORM support 14 | - Multiple ORM entity managers 15 | - Multiple DBAL connections 16 | - Reuse existing PDO connections in DBAL connection 17 | 18 | ## Installation 19 | 20 | Run the following to install this library using [Composer](https://getcomposer.org/): 21 | 22 | ```bash 23 | composer require doctrine/doctrine-orm-module 24 | ``` 25 | 26 | ## Documentation 27 | 28 | Please check the [documentation on the Doctrine website](https://www.doctrine-project.org/projects/doctrine-orm-module.html) 29 | for more detailed information on features provided by this component. The source files for the documentation can be 30 | found in the [docs directory](./docs/en). 31 | -------------------------------------------------------------------------------- /src/Collector/SQLLoggerCollector.php: -------------------------------------------------------------------------------- 1 | name; 31 | } 32 | 33 | public function getPriority(): int 34 | { 35 | return self::PRIORITY; 36 | } 37 | 38 | public function collect(MvcEvent $mvcEvent): void 39 | { 40 | } 41 | 42 | public function canHide(): bool 43 | { 44 | return empty($this->sqlLogger->queries); 45 | } 46 | 47 | public function getQueryCount(): int 48 | { 49 | return count($this->sqlLogger->queries); 50 | } 51 | 52 | /** @return mixed[] */ 53 | public function getQueries(): array 54 | { 55 | return $this->sqlLogger->queries; 56 | } 57 | 58 | public function getQueryTime(): float 59 | { 60 | $time = 0.0; 61 | 62 | foreach ($this->sqlLogger->queries as $query) { 63 | $time += $query['executionMS']; 64 | } 65 | 66 | return $time; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Options/DBALConfiguration.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DBALConfiguration extends AbstractOptions 15 | { 16 | /** 17 | * Set the cache key for the result cache. Cache key 18 | * is assembled as "doctrine.cache.{key}" and pulled from 19 | * service locator. 20 | */ 21 | protected string $resultCache = 'array'; 22 | 23 | /** 24 | * Set the class name of the SQL Logger, or null, to disable. 25 | */ 26 | protected string|null $sqlLogger = null; 27 | 28 | /** 29 | * Keys must be the name of the type identifier and value is 30 | * the class name of the Type 31 | * 32 | * @var mixed[] 33 | */ 34 | protected array $types = []; 35 | 36 | public function setResultCache(string $resultCache): void 37 | { 38 | $this->resultCache = $resultCache; 39 | } 40 | 41 | public function getResultCache(): string 42 | { 43 | return 'doctrine.cache.' . $this->resultCache; 44 | } 45 | 46 | public function setSqlLogger(string $sqlLogger): void 47 | { 48 | $this->sqlLogger = $sqlLogger; 49 | } 50 | 51 | public function getSqlLogger(): string|null 52 | { 53 | return $this->sqlLogger; 54 | } 55 | 56 | /** @param mixed[] $types */ 57 | public function setTypes(array $types): void 58 | { 59 | $this->types = $types; 60 | } 61 | 62 | public function getTypes(): mixed 63 | { 64 | return $this->types; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Options/SQLLoggerCollectorOptions.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class SQLLoggerCollectorOptions extends AbstractOptions 15 | { 16 | /** @var string name to be assigned to the collector */ 17 | protected string $name = 'orm_default'; 18 | 19 | /** @var string|null service name of the configuration where the logger has to be put */ 20 | protected string|null $configuration = null; 21 | 22 | /** @var string|null service name of the SQLLogger to be used */ 23 | protected string|null $sqlLogger = null; 24 | 25 | public function setName(string|null $name): void 26 | { 27 | $this->name = (string) $name; 28 | } 29 | 30 | /** 31 | * Name of the collector 32 | */ 33 | public function getName(): string 34 | { 35 | return $this->name; 36 | } 37 | 38 | public function setConfiguration(string|null $configuration): void 39 | { 40 | $this->configuration = $configuration ?: null; 41 | } 42 | 43 | /** 44 | * Configuration service name (where to set the logger) 45 | */ 46 | public function getConfiguration(): string 47 | { 48 | return $this->configuration ?: 'doctrine.configuration.orm_default'; 49 | } 50 | 51 | public function setSqlLogger(string|null $sqlLogger): void 52 | { 53 | $this->sqlLogger = $sqlLogger ?: null; 54 | } 55 | 56 | /** 57 | * SQLLogger service name 58 | */ 59 | public function getSqlLogger(): string|null 60 | { 61 | return $this->sqlLogger; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ci/Version20120714005702.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | namespace CiDoctrineMigrations; 21 | 22 | use Doctrine\DBAL\Schema\Schema, 23 | Doctrine\Migrations\AbstractMigration; 24 | 25 | /** 26 | * Empty test migration used for testing 27 | */ 28 | class Version20120714005702 extends AbstractMigration 29 | { 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | public function up(Schema $schema) : void 34 | { 35 | $this->addSql("CREATE TABLE test (id int)"); 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | public function down(Schema $schema) : void 42 | { 43 | $this->addSql("DROP TABLE test"); 44 | } 45 | 46 | public function isTransactional(): bool 47 | { 48 | return false; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Options/EntityResolver.php: -------------------------------------------------------------------------------- 1 | */ 14 | final class EntityResolver extends AbstractOptions 15 | { 16 | /** 17 | * Set the configuration key for the EventManager. Event manager key 18 | * is assembled as "doctrine.eventmanager.{key}" and pulled from 19 | * service locator. 20 | */ 21 | protected string $eventManager = 'orm_default'; 22 | 23 | /** 24 | * An array that maps a class name (or interface name) to another class 25 | * name 26 | * 27 | * @var mixed[] 28 | */ 29 | protected array $resolvers = []; 30 | 31 | public function setEventManager(string $eventManager): self 32 | { 33 | $this->eventManager = $eventManager; 34 | 35 | return $this; 36 | } 37 | 38 | public function getEventManager(): string 39 | { 40 | return 'doctrine.eventmanager.' . $this->eventManager; 41 | } 42 | 43 | /** 44 | * @param mixed[] $resolvers 45 | * 46 | * @throws InvalidArgumentException 47 | */ 48 | public function setResolvers(array $resolvers): void 49 | { 50 | foreach ($resolvers as $old => $new) { 51 | if (! class_exists($new)) { 52 | throw new InvalidArgumentException( 53 | sprintf( 54 | '%s is resolved to the entity %s, which does not exist', 55 | $old, 56 | $new, 57 | ), 58 | ); 59 | } 60 | 61 | $this->resolvers[$old] = $new; 62 | } 63 | } 64 | 65 | /** @return mixed[] */ 66 | public function getResolvers(): array 67 | { 68 | return $this->resolvers; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Options/EntityManager.php: -------------------------------------------------------------------------------- 1 | */ 10 | final class EntityManager extends AbstractOptions 11 | { 12 | /** 13 | * Set the configuration key for the Configuration. Configuration key 14 | * is assembled as "doctrine.configuration.{key}" and pulled from 15 | * service locator. 16 | */ 17 | protected string $configuration = 'orm_default'; 18 | 19 | /** 20 | * Set the connection key for the Connection. Connection key 21 | * is assembled as "doctrine.connection.{key}" and pulled from 22 | * service locator. 23 | */ 24 | protected string $connection = 'orm_default'; 25 | 26 | /** 27 | * Set the connection key for the EntityResolver, which is 28 | * a service of type {@see \Doctrine\ORM\Tools\ResolveTargetEntityListener}. 29 | * The EntityResolver service name is assembled 30 | * as "doctrine.entity_resolver.{key}" 31 | */ 32 | protected string $entityResolver = 'orm_default'; 33 | 34 | public function setConfiguration(string $configuration): self 35 | { 36 | $this->configuration = $configuration; 37 | 38 | return $this; 39 | } 40 | 41 | public function getConfiguration(): string 42 | { 43 | return 'doctrine.configuration.' . $this->configuration; 44 | } 45 | 46 | public function setConnection(string $connection): self 47 | { 48 | $this->connection = $connection; 49 | 50 | return $this; 51 | } 52 | 53 | public function getConnection(): string 54 | { 55 | return 'doctrine.connection.' . $this->connection; 56 | } 57 | 58 | public function setEntityResolver(string $entityResolver): self 59 | { 60 | $this->entityResolver = $entityResolver; 61 | 62 | return $this; 63 | } 64 | 65 | public function getEntityResolver(): string 66 | { 67 | return 'doctrine.entity_resolver.' . $this->entityResolver; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Paginator/Adapter/DoctrinePaginator.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class DoctrinePaginator implements AdapterInterface, JsonSerializable 21 | { 22 | /** 23 | * Constructor 24 | * 25 | * @param Paginator $paginator 26 | */ 27 | public function __construct(protected Paginator $paginator) 28 | { 29 | } 30 | 31 | /** @param Paginator $paginator */ 32 | public function setPaginator(Paginator $paginator): self 33 | { 34 | $this->paginator = $paginator; 35 | 36 | return $this; 37 | } 38 | 39 | /** @return Paginator */ 40 | public function getPaginator(): Paginator 41 | { 42 | return $this->paginator; 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | * 48 | * @param int|null $offset 49 | * @param int|null $itemCountPerPage 50 | * 51 | * @return ArrayIterator 52 | */ 53 | public function getItems($offset, $itemCountPerPage) 54 | { 55 | $this->paginator 56 | ->getQuery() 57 | ->setFirstResult($offset) 58 | ->setMaxResults($itemCountPerPage); 59 | 60 | return $this->paginator->getIterator(); 61 | } 62 | 63 | public function count(): int 64 | { 65 | return $this->paginator->count(); 66 | } 67 | 68 | /** @return array{select: list|string, count_select: int} */ 69 | public function jsonSerialize(): array 70 | { 71 | return [ 72 | 'select' => $this->paginator->getQuery()->getSQL(), 73 | 'count_select' => $this->paginator->count(), 74 | ]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ci/config/ci.config.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'connection' => [ 8 | 'orm_default' => [ 9 | 'params' => [ 10 | 'host' => '127.0.0.1', 11 | 'user' => 'root', 12 | 'password' => '', 13 | 'dbname' => 'database', 14 | ], 15 | ], 16 | ], 17 | 'configuration' => [ 18 | 'orm_default' => [ 19 | 'metadata_cache' => 'filesystem', 20 | 'query_cache' => 'filesystem', 21 | 'generate_proxies' => false, 22 | 'proxy_dir' => 'ci/cache/DoctrineORMModule/Proxy', 23 | ], 24 | ], 25 | 'migrations_configuration' => [ 26 | 'orm_default' => [ 27 | 'table_storage' => [ 28 | 'table_name' => 'DoctrineMigrationVersions', 29 | 'version_column_name' => 'version', 30 | 'version_column_length' => 191, 31 | 'executed_at_column_name' => 'executedAt', 32 | 'execution_time_column_name' => 'executionTime', 33 | ], 34 | 'migrations_paths' => [ 35 | 'CiDoctrineMigrations' => 'ci', 36 | ], 37 | 'migrations' => [], 38 | 'all_or_nothing' => false, 39 | 'transactional' => false, 40 | 'check_database_platform' => true, 41 | 'organize_migrations' => 'year', // year or year_and_month 42 | 'custom_template' => null, 43 | ], 44 | ], 45 | 'cache' => [ 46 | 'filesystem' => [ 47 | 'directory' => 'ci/cache/DoctrineModule', 48 | ], 49 | ], 50 | 'driver' => [ 51 | 'ci_driver' => [ 52 | 'class' => Doctrine\ORM\Mapping\Driver\AnnotationDriver::class, 53 | 'cache' => 'array', 54 | 'paths' => ['ci/Entity/'], 55 | ], 56 | 'orm_default' => [ 57 | 'drivers' => [ 58 | 'DoctrineORMModule\Ci\Entity' => 'ci_driver', 59 | ], 60 | ], 61 | ], 62 | ], 63 | ]; 64 | -------------------------------------------------------------------------------- /src/Collector/MappingCollector.php: -------------------------------------------------------------------------------- 1 | classMetadataFactory = $classMetadataFactory; 33 | } 34 | 35 | public function getName(): string 36 | { 37 | return $this->name; 38 | } 39 | 40 | public function getPriority(): int 41 | { 42 | return self::PRIORITY; 43 | } 44 | 45 | public function collect(MvcEvent $mvcEvent): void 46 | { 47 | if (! $this->classMetadataFactory) { 48 | return; 49 | } 50 | 51 | /** @var ClassMetadata[] $metadata */ 52 | $metadata = $this->classMetadataFactory->getAllMetadata(); 53 | $this->classes = []; 54 | 55 | foreach ($metadata as $class) { 56 | $this->classes[$class->getName()] = $class; 57 | } 58 | 59 | ksort($this->classes); 60 | } 61 | 62 | public function canHide(): bool 63 | { 64 | return empty($this->classes); 65 | } 66 | 67 | /** @return array{name: string, classes: ClassMetadata[]} */ 68 | public function __serialize(): array 69 | { 70 | return [ 71 | 'name' => $this->name, 72 | 'classes' => $this->classes, 73 | ]; 74 | } 75 | 76 | /** @param array{name: string, classes: ClassMetadata[]} $data */ 77 | public function __unserialize(array $data): void 78 | { 79 | $this->name = $data['name']; 80 | $this->classes = $data['classes']; 81 | } 82 | 83 | /** @return ClassMetadata[] */ 84 | public function getClasses(): array 85 | { 86 | return $this->classes; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Options/SecondLevelCacheConfiguration.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class SecondLevelCacheConfiguration extends AbstractOptions 15 | { 16 | /** 17 | * Enable the second level cache configuration 18 | */ 19 | protected bool $enabled = false; 20 | 21 | /** 22 | * Default lifetime 23 | */ 24 | protected int $defaultLifetime = 3600; 25 | 26 | /** 27 | * Default lock lifetime 28 | */ 29 | protected int $defaultLockLifetime = 60; 30 | 31 | /** 32 | * The file lock region directory (needed for some cache usage) 33 | */ 34 | protected string $fileLockRegionDirectory = ''; 35 | 36 | /** 37 | * Configure the lifetime and lock lifetime per region. You must pass an associative array like this: 38 | * 39 | * [ 40 | * 'My\Region' => ['lifetime' => 200, 'lock_lifetime' => 400], 41 | * ] 42 | * 43 | * @var mixed[] 44 | */ 45 | protected array $regions = []; 46 | 47 | public function setEnabled(bool $enabled): void 48 | { 49 | $this->enabled = $enabled; 50 | } 51 | 52 | public function isEnabled(): bool 53 | { 54 | return $this->enabled; 55 | } 56 | 57 | public function setDefaultLifetime(int $defaultLifetime): void 58 | { 59 | $this->defaultLifetime = $defaultLifetime; 60 | } 61 | 62 | public function getDefaultLifetime(): int 63 | { 64 | return $this->defaultLifetime; 65 | } 66 | 67 | public function setDefaultLockLifetime(int $defaultLockLifetime): void 68 | { 69 | $this->defaultLockLifetime = $defaultLockLifetime; 70 | } 71 | 72 | public function getDefaultLockLifetime(): int 73 | { 74 | return $this->defaultLockLifetime; 75 | } 76 | 77 | public function setFileLockRegionDirectory(string $fileLockRegionDirectory): void 78 | { 79 | $this->fileLockRegionDirectory = $fileLockRegionDirectory; 80 | } 81 | 82 | public function getFileLockRegionDirectory(): string 83 | { 84 | return $this->fileLockRegionDirectory; 85 | } 86 | 87 | /** @param mixed[] $regions */ 88 | public function setRegions(array $regions): void 89 | { 90 | $this->regions = $regions; 91 | } 92 | 93 | /** @return mixed[] */ 94 | public function getRegions(): array 95 | { 96 | return $this->regions; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | getEventManager() 31 | ->getSharedManager() 32 | ->attach( 33 | 'doctrine', 34 | 'loadCli.post', 35 | static function (EventInterface $event): void { 36 | $event 37 | ->getParam('ServiceManager') 38 | ->get(CliConfigurator::class) 39 | ->configure($event->getTarget()); 40 | }, 41 | 1, 42 | ); 43 | 44 | // Initialize logger collector in DeveloperTools 45 | if (! class_exists(ProfilerEvent::class)) { 46 | return; 47 | } 48 | 49 | $manager 50 | ->getEventManager() 51 | ->attach( 52 | ProfilerEvent::EVENT_PROFILER_INIT, 53 | /** @param EventInterface $event */ 54 | static function ($event): void { 55 | $container = $event->getTarget()->getParam('ServiceManager'); 56 | $container->get('doctrine.sql_logger_collector.orm_default'); 57 | }, 58 | ); 59 | } 60 | 61 | /** 62 | * {@inheritDoc} 63 | * 64 | * @return array 65 | */ 66 | public function getConfig(): array 67 | { 68 | return include __DIR__ . '/../config/module.config.php'; 69 | } 70 | 71 | /** 72 | * {@inheritDoc} 73 | * 74 | * @return array 75 | */ 76 | public function getControllerConfig(): array 77 | { 78 | return include __DIR__ . '/../config/controllers.config.php'; 79 | } 80 | 81 | /** 82 | * {@inheritDoc} 83 | * 84 | * @return array 85 | */ 86 | public function getModuleDependencies(): array 87 | { 88 | return ['DoctrineModule']; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Service/SQLLoggerCollectorFactory.php: -------------------------------------------------------------------------------- 1 | getOptions($serviceLocator); 32 | 33 | // @todo always ask the serviceLocator instead? (add a factory?) 34 | if ($options->getSqlLogger()) { 35 | $debugStackLogger = $serviceLocator->get($options->getSqlLogger()); 36 | } else { 37 | $debugStackLogger = new DebugStack(); 38 | } 39 | 40 | $configuration = $serviceLocator->get($options->getConfiguration()); 41 | 42 | if ($configuration->getSQLLogger() !== null) { 43 | $logger = new LoggerChain([ 44 | $debugStackLogger, 45 | $configuration->getSQLLogger(), 46 | ]); 47 | $configuration->setSQLLogger($logger); 48 | } else { 49 | $configuration->setSQLLogger($debugStackLogger); 50 | } 51 | 52 | return new SQLLoggerCollector($debugStackLogger, 'doctrine.sql_logger_collector.' . $options->getName()); 53 | } 54 | 55 | /** @throws RuntimeException */ 56 | protected function getOptions(ContainerInterface $serviceLocator): mixed 57 | { 58 | $options = $serviceLocator->get('config'); 59 | $options = $options['doctrine']; 60 | $options = $options['sql_logger_collector'][$this->name] ?? null; 61 | 62 | if ($options === null) { 63 | throw new RuntimeException( 64 | sprintf( 65 | 'Configuration with name "%s" could not be found in "doctrine.sql_logger_collector".', 66 | $this->name, 67 | ), 68 | ); 69 | } 70 | 71 | $optionsClass = $this->getOptionsClass(); 72 | 73 | return new $optionsClass($options); 74 | } 75 | 76 | /** @phpstan-return class-string */ 77 | protected function getOptionsClass(): string 78 | { 79 | return SQLLoggerCollectorOptions::class; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Service/DBALConnectionFactory.php: -------------------------------------------------------------------------------- 1 | getOptions($serviceLocator, 'connection'); 35 | assert($options instanceof DBALConnection); 36 | $pdo = $options->getPdo(); 37 | 38 | if (is_string($pdo)) { 39 | $pdo = $serviceLocator->get($pdo); 40 | assert($pdo instanceof PDO); 41 | } 42 | 43 | $params = [ 44 | 'driverClass' => $options->getDriverClass(), 45 | 'wrapperClass' => $options->getWrapperClass(), 46 | 'pdo' => $pdo, 47 | ]; 48 | $params = array_merge($params, $options->getParams()); 49 | 50 | if ( 51 | array_key_exists('platform', $params) 52 | && is_string($params['platform']) 53 | && $serviceLocator->has($params['platform']) 54 | ) { 55 | $params['platform'] = $serviceLocator->get($params['platform']); 56 | } 57 | 58 | $configuration = $serviceLocator->get($options->getConfiguration()); 59 | $eventManager = $serviceLocator->get($options->getEventManager()); 60 | 61 | $connection = DriverManager::getConnection($params, $configuration, $eventManager); 62 | foreach ($options->getDoctrineTypeMappings() as $dbType => $doctrineType) { 63 | $connection->getDatabasePlatform()->registerDoctrineTypeMapping($dbType, $doctrineType); 64 | } 65 | 66 | foreach ($options->getDoctrineCommentedTypes() as $type) { 67 | $connection->getDatabasePlatform()->markDoctrineTypeCommented(Type::getType($type)); 68 | } 69 | 70 | if ($options->useSavepoints()) { 71 | $connection->setNestTransactionsWithSavepoints(true); 72 | } 73 | 74 | return $connection; 75 | } 76 | 77 | /** 78 | * Get the class name of the options associated with this factory. 79 | */ 80 | public function getOptionsClass(): string 81 | { 82 | return DBALConnection::class; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.doctrine-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "active": true, 3 | "name": "Doctrine ORM Module for Laminas", 4 | "slug": "doctrine-orm-module", 5 | "versions": [ 6 | { 7 | "name": "6.4", 8 | "branchName": "6.4.x", 9 | "slug": "6.4", 10 | "aliases": [ 11 | "latest" 12 | ], 13 | "upcoming": true 14 | }, 15 | { 16 | "name": "6.3", 17 | "branchName": "6.3.x", 18 | "slug": "6.3", 19 | "aliases": [ 20 | "current", 21 | "stable" 22 | ], 23 | "current": true, 24 | "maintained": true 25 | }, 26 | { 27 | "name": "6.2", 28 | "branchName": "6.2.x", 29 | "slug": "6.2", 30 | "maintained": false 31 | }, 32 | { 33 | "name": "6.1", 34 | "branchName": "6.1.x", 35 | "slug": "6.1", 36 | "maintained": false 37 | }, 38 | { 39 | "name": "6.0", 40 | "branchName": "6.0.x", 41 | "slug": "6.0", 42 | "maintained": false 43 | }, 44 | { 45 | "name": "5.3", 46 | "branchName": "5.3.x", 47 | "slug": "5.3", 48 | "maintained": false 49 | }, 50 | { 51 | "name": "5.2", 52 | "branchName": "5.2.x", 53 | "slug": "5.2", 54 | "maintained": false 55 | }, 56 | { 57 | "name": "5.1", 58 | "branchName": "5.1.x", 59 | "slug": "5.1", 60 | "maintained": false 61 | }, 62 | { 63 | "name": "5.0", 64 | "branchName": "5.0.x", 65 | "slug": "5.0", 66 | "maintained": false 67 | }, 68 | { 69 | "name": "4.2", 70 | "branchName": "4.2.x", 71 | "slug": "4.2", 72 | "maintained": false 73 | }, 74 | { 75 | "name": "4.1", 76 | "branchName": "4.1.x", 77 | "slug": "4.1", 78 | "maintained": false 79 | }, 80 | { 81 | "name": "4.0", 82 | "branchName": "4.0.x", 83 | "slug": "4.0", 84 | "maintained": false 85 | }, 86 | { 87 | "name": "3.2", 88 | "branchName": "3.2.x", 89 | "slug": "3.2", 90 | "maintained": false 91 | }, 92 | { 93 | "name": "3.1", 94 | "branchName": "3.1.x", 95 | "slug": "3.1", 96 | "maintained": false 97 | }, 98 | { 99 | "name": "3.0", 100 | "branchName": "3.0.x", 101 | "slug": "3.0", 102 | "maintained": false 103 | }, 104 | { 105 | "name": "2.1", 106 | "branchName": "2.1.x", 107 | "slug": "2.1", 108 | "maintained": false 109 | }, 110 | { 111 | "name": "1.1", 112 | "branchName": "1.1.x", 113 | "slug": "1.1", 114 | "maintained": false 115 | }, 116 | { 117 | "name": "1.0", 118 | "branchName": "1.0.x", 119 | "slug": "1.0", 120 | "maintained": false 121 | } 122 | ] 123 | } 124 | -------------------------------------------------------------------------------- /docs/en/migrations.rst: -------------------------------------------------------------------------------- 1 | Doctrine Migrations 2 | =================== 3 | 4 | Support for the migrations library ^3.0 is included. You may create one 5 | migration configuration for each object manager. 6 | See the `module documentation `__ for 7 | more information. 8 | 9 | Configure 10 | --------- 11 | 12 | .. code:: php 13 | 14 | return [ 15 | 'doctrine' => [ 16 | 'migrations_configuration' => [ 17 | 'orm_default' => [ 18 | 'table_storage' => [ 19 | 'table_name' => 'DoctrineMigrationVersions', 20 | 'version_column_name' => 'version', 21 | 'version_column_length' => 191, 22 | 'executed_at_column_name' => 'executedAt', 23 | 'execution_time_column_name' => 'executionTime', 24 | ], 25 | 'migrations_paths' => [], // an array of namespace => path 26 | 'migrations' => [], // an array of fully qualified migrations 27 | 'all_or_nothing' => false, 28 | 'check_database_platform' => true, 29 | 'organize_migrations' => 'year', // year or year_and_month 30 | 'custom_template' => null, 31 | ], 32 | 'orm_other' => [ 33 | ... 34 | ] 35 | ], 36 | ], 37 | ]; 38 | 39 | Set a Custom configuration into DependencyFactory 40 | ------------------------------- 41 | 42 | .. code:: php 43 | 44 | return [ 45 | 'doctrine' => [ 46 | 'migrations_configuration' => [ 47 | 'orm_default' => [ 48 | 'dependency_factory_services' => [ 49 | 'service_to_overwrite' => 'custom_service_id' 50 | ], 51 | ], 52 | ], 53 | ], 54 | ]; 55 | 56 | Note : 'custom_service_id' has to be defined in your DIC 57 | 58 | 59 | This configuration allows you, for example, to define a custom version comparator 60 | 61 | .. code:: php 62 | 63 | return [ 64 | 'doctrine' => [ 65 | 'migrations_configuration' => [ 66 | 'orm_default' => [ 67 | 'dependency_factory_services' => [ 68 | \Doctrine\Migrations\Version\Comparator::class => MyComparator::class 69 | ], 70 | ], 71 | ], 72 | ], 73 | ]; 74 | 75 | List of services that can be overwritten 76 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 77 | 78 | - Doctrine\\Migrations\\Finder\\MigrationFinder 79 | - Doctrine\\Migrations\\Metadata\\Storage\\MetadataStorage 80 | - Doctrine\\Migrations\\MigrationsRepository 81 | - Doctrine\\Migrations\\Provider\\SchemaProvider 82 | - Doctrine\\Migrations\\Tools\\Console\\MigratorConfigurationFactory 83 | - Doctrine\\Migrations\\Version\\Comparator 84 | - Doctrine\\Migrations\\Version\\MigrationFactory 85 | - Doctrine\\Migrations\\Version\\MigrationPlanCalculator 86 | - Doctrine\\Migrations\\Version\\MigrationStatusCalculator 87 | - Psr\\Log\\LoggerInterface 88 | - Symfony\\Component\\Stopwatch\\Stopwatch -------------------------------------------------------------------------------- /docs/en/forms.rst: -------------------------------------------------------------------------------- 1 | Laminas Forms 2 | ============= 3 | 4 | DoctrineModule and DoctrineORMModule provide an integration with `laminas-form `_. 5 | 6 | Creating Forms using Entity Annotations 7 | --------------------------------------- 8 | 9 | With laminas-form, forms can be created using `PHP8 attributes or DocBlock annotations `_. 10 | DoctrineORMModule extends this feature to support Doctrine-specific form elements (see next section). 11 | 12 | First, create a form builder instance. By default, this uses the ``AnnotationBuilder`` from laminas-form, 13 | which uses DocBlock annotations. Alternatively, you can provide an ``AttributeBuilder`` to use PHP8-style 14 | attributes. 15 | 16 | .. code:: php 17 | 18 | // using PhpDoc annotations 19 | $entityManager = $container->get(\Doctrine\ORM\EntityManager::class); 20 | $builder = new \DoctrineORMModule\Form\Annotation\EntityBasedFormBuilder($entityManager); 21 | 22 | // alternatively, to use PHP8 attributes 23 | $entityManager = $container->get(\Doctrine\ORM\EntityManager::class); 24 | $attributeBuilder = new \Laminas\Form\Annotation\AttributeBuilder(); 25 | $builder = new \DoctrineORMModule\Form\Annotation\EntityBasedFormBuilder($entityManager, $attributeBuilder); 26 | 27 | Given an entity instance, the form builder can either create a form specification or directly a form instance: 28 | 29 | .. code:: php 30 | 31 | $entity = new User(); 32 | 33 | // get form specification only 34 | $formSpec = $builder->getFormSpecification($entity); 35 | 36 | // or directly get form 37 | $form= $builder->createForm($entity); 38 | 39 | Extension points for customizing the form builder are the event manager and the form factory, which can 40 | be accessed as follows: 41 | 42 | .. code:: php 43 | 44 | // if you need access to the event manager 45 | $myListener = new MyListener(); 46 | $myListener->attach($builder->getBuilder()->getEventManager()); 47 | 48 | // if you need access to the form factory 49 | $formElementManager = $container->get(\Laminas\Form\FormElementManager::class) 50 | $builder->getBuilder()->getFormFactory()->setFormElementManager($formElementManager); 51 | 52 | Doctrine-specific Form Elements 53 | ------------------------------- 54 | 55 | DoctrineModule provides three Doctrine-specific form elements: 56 | 57 | - ``DoctrineModule\Form\Element\ObjectSelect`` 58 | - ``DoctrineModule\Form\Element\ObjectRadio`` 59 | - ``DoctrineModule\Form\Element\ObjectMultiCheckbox`` 60 | 61 | Please read the `DoctrineModule documentation on form elements `_ 62 | for further information. 63 | 64 | Doctrine-specific Validators 65 | ---------------------------- 66 | 67 | DoctrineModule provides three Doctrine-specific validators: 68 | 69 | - ``DoctrineModule\Validator\ObjectExists`` 70 | - ``DoctrineModule\Validator\NoObjectExists`` 71 | - ``DoctrineModule\Validator\UniqueObject`` 72 | 73 | Please read the `DoctrineModule documentation on validators `_ 74 | for further information. 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doctrine/doctrine-orm-module", 3 | "description": "Laminas Module that provides Doctrine ORM functionality", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "doctrine", 8 | "orm", 9 | "module", 10 | "laminas" 11 | ], 12 | "homepage": "https://www.doctrine-project.org/", 13 | "require": { 14 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", 15 | "ext-json": "*", 16 | "doctrine/dbal": "^3.3.2", 17 | "doctrine/doctrine-laminas-hydrator": "^3.2.0", 18 | "doctrine/doctrine-module": "^6.3.0", 19 | "doctrine/event-manager": "^2.0.0", 20 | "doctrine/orm": "^2.13.0", 21 | "doctrine/persistence": "^3.0.0", 22 | "laminas/laminas-eventmanager": "^3.5.0", 23 | "laminas/laminas-modulemanager": "^2.11.0", 24 | "laminas/laminas-mvc": "^3.3.5", 25 | "laminas/laminas-paginator": "^2.13.0", 26 | "laminas/laminas-servicemanager": "^3.17.0", 27 | "laminas/laminas-stdlib": "^3.13.0", 28 | "psr/container": "^1.1.2", 29 | "symfony/console": "^6.1.2 || ^7.0.0" 30 | }, 31 | "require-dev": { 32 | "doctrine/annotations": "^2.0.0", 33 | "doctrine/coding-standard": "^12.0.0", 34 | "doctrine/data-fixtures": "^2.0.1", 35 | "doctrine/migrations": "^3.8.0", 36 | "laminas/laminas-cache-storage-adapter-filesystem": "^2.0", 37 | "laminas/laminas-cache-storage-adapter-memory": "^2.0", 38 | "laminas/laminas-developer-tools": "^2.3.0", 39 | "laminas/laminas-i18n": "^2.23.0", 40 | "laminas/laminas-serializer": "^2.12.0", 41 | "phpstan/phpstan": "^2.0.4", 42 | "phpstan/phpstan-phpunit": "^2.0.3", 43 | "phpunit/phpunit": "^10.5.40" 44 | }, 45 | "conflict": { 46 | "doctrine/data-fixtures": "<2.0", 47 | "doctrine/migrations": "<3.8", 48 | "laminas/laminas-form": "<3.10" 49 | }, 50 | "suggest": { 51 | "doctrine/migrations": "doctrine migrations if you want to keep your schema definitions versioned", 52 | "laminas/laminas-developer-tools": "laminas-developer-tools if you want to profile operations executed by the ORM during development", 53 | "laminas/laminas-form": "if you want to use form elements backed by Doctrine" 54 | }, 55 | "autoload": { 56 | "psr-4": { 57 | "DoctrineORMModule\\": "src/" 58 | } 59 | }, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "DoctrineORMModuleTest\\": "tests/" 63 | } 64 | }, 65 | "config": { 66 | "allow-plugins": { 67 | "composer/package-versions-deprecated": true, 68 | "dealerdirect/phpcodesniffer-composer-installer": true 69 | }, 70 | "sort-packages": true 71 | }, 72 | "extra": { 73 | "laminas": { 74 | "config-provider": "DoctrineORMModule\\ConfigProvider", 75 | "module": "DoctrineORMModule" 76 | } 77 | }, 78 | "scripts": { 79 | "check": [ 80 | "@cs-check", 81 | "@phpstan", 82 | "@test" 83 | ], 84 | "cs-check": "phpcs", 85 | "cs-fix": "phpcbf", 86 | "phpstan": "phpstan analyse", 87 | "test": "phpunit --colors=always", 88 | "test-coverage": "phpunit --colors=always --coverage-clover=coverage.xml" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /docs/en/miscellaneous.rst: -------------------------------------------------------------------------------- 1 | Miscellaneous 2 | ============= 3 | 4 | The items listed below are optional and intended to enhance 5 | integration between Laminas and Doctrine ORM. 6 | 7 | Authentication Adapter 8 | ---------------------- 9 | 10 | The authentication adapter is intended to provide an adapter for ``Laminas\Authentication``. It works much 11 | like the ``DbTable`` adapter in the core framework. You must provide the 12 | entity manager instance, entity name, identity field, and credential 13 | field. You can optionally provide a callable method to perform hashing 14 | on the password prior to checking for validation. 15 | 16 | .. code:: php 17 | 18 | getSalt(), 31 | $identity->getAlgorithm() 32 | ); 33 | } 34 | ); 35 | 36 | $adapter->setIdentityValue('admin'); 37 | $adapter->setCredentialValue('password'); 38 | $result = $adapter->authenticate(); 39 | 40 | echo $result->isValid() ? 'Authenticated' : 'Could not authenticate'; 41 | 42 | 43 | Custom DBAL Types 44 | ----------------- 45 | 46 | To register custom Doctrine DBAL types add them to the 47 | ``doctrine.configuration.orm_default.types`` key in you 48 | configuration file: 49 | 50 | .. code:: php 51 | 52 | [ 55 | 'configuration' => [ 56 | 'orm_default' => [ 57 | 'types' => [ 58 | // You can override a default type 59 | 'date' => 'My\DBAL\Types\DateType', 60 | 61 | // And set new ones 62 | 'tinyint' => 'My\DBAL\Types\TinyIntType', 63 | ], 64 | ], 65 | ], 66 | ], 67 | ]; 68 | 69 | With this configuration you may use them in your ORM entities 70 | to define field datatypes: 71 | 72 | .. code:: php 73 | 74 | [ 98 | 'connection' => [ 99 | 'orm_default' => [ 100 | 'doctrine_type_mappings' => [ 101 | 'tinyint' => 'tinyint', 102 | ], 103 | ], 104 | ], 105 | ], 106 | ]; 107 | 108 | Now using Schema-Tool, whenever it finds a column of type "tinyint" 109 | it will convert it into a "tinyint" Doctrine Type instance for Schema 110 | representation. Keep in mind that you can easily produce clashes this 111 | way because each database type can only map to exactly one Doctrine mapping 112 | type. 113 | -------------------------------------------------------------------------------- /view/laminas-developer-tools/toolbar/doctrine-orm-mappings.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Doctrine ORM (Mappings) 5 | 6 | getClasses()); ?> 7 | mappings 8 | 9 |
10 |
11 | 12 | 13 | DoctrineORMModule 14 | 15 | 16 | 17 | Mappings for 18 | escapeHtml($collector->getName()); ?> 19 | 20 |
21 | generateFromMetadata($collector->getClasses()); 24 | ?> 25 | 26 | 27 | 28 | 29 |
30 | 31 | getClasses() as $class): ?> 32 | escapeHtml($class->getName()); ?> 33 |
34 | 35 |
36 |
37 |
38 | 72 | -------------------------------------------------------------------------------- /src/Service/DBALConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | setupDBALConfiguration($serviceLocator, $config); 41 | 42 | return $config; 43 | } 44 | 45 | public function setupDBALConfiguration(ContainerInterface $serviceLocator, Configuration $config): void 46 | { 47 | $options = $this->getOptions($serviceLocator); 48 | $config->setResultCacheImpl($serviceLocator->get($options->resultCache)); 49 | 50 | $sqlLogger = $options->sqlLogger; 51 | if (is_string($sqlLogger) && $serviceLocator->has($sqlLogger)) { 52 | $sqlLogger = $serviceLocator->get($sqlLogger); 53 | } 54 | 55 | $config->setSQLLogger($sqlLogger); 56 | 57 | if (method_exists($config, 'setMiddlewares')) { 58 | $middlewares = []; 59 | foreach ($options->middlewares as $middlewareName) { 60 | if (! is_string($middlewareName) || ! $serviceLocator->has($middlewareName)) { 61 | throw new InvalidArgumentException('Middleware not exists'); 62 | } 63 | 64 | $middleware = $serviceLocator->get($middlewareName); 65 | if (! $middleware instanceof Middleware) { 66 | throw new UnexpectedValueException(sprintf( 67 | 'Invalid middleware with %s name. %s expected.', 68 | $middlewareName, 69 | Middleware::class, 70 | )); 71 | } 72 | 73 | $middlewares[] = $middleware; 74 | } 75 | 76 | $config->setMiddlewares($middlewares); 77 | } 78 | 79 | foreach ($options->types as $name => $class) { 80 | if (Type::hasType($name)) { 81 | Type::overrideType($name, $class); 82 | } else { 83 | Type::addType($name, $class); 84 | } 85 | } 86 | } 87 | 88 | /** @throws RuntimeException */ 89 | public function getOptions(ContainerInterface $serviceLocator): mixed 90 | { 91 | $options = $serviceLocator->get('config'); 92 | $options = $options['doctrine']; 93 | $options = $options['configuration'][$this->name] ?? null; 94 | 95 | if ($options === null) { 96 | throw new RuntimeException( 97 | sprintf( 98 | 'Configuration with name "%s" could not be found in "doctrine.configuration".', 99 | $this->name, 100 | ), 101 | ); 102 | } 103 | 104 | $optionsClass = $this->getOptionsClass(); 105 | 106 | return new $optionsClass($options); 107 | } 108 | 109 | protected function getOptionsClass(): string 110 | { 111 | return DoctrineORMModuleConfiguration::class; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Service/MigrationsCommandFactory.php: -------------------------------------------------------------------------------- 1 | commandClassName = 'Doctrine\Migrations\Tools\Console\Command\\' . $name . 'Command'; 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | * 56 | * @param string $requestedName 57 | * 58 | * @return DoctrineCommand 59 | * 60 | * @throws InvalidArgumentException 61 | */ 62 | public function __invoke(ContainerInterface $serviceLocator, $requestedName, array|null $options = null) 63 | { 64 | $commandClassName = $this->commandClassName; 65 | 66 | if (! class_exists($commandClassName)) { 67 | throw new InvalidArgumentException('The class ' . $commandClassName . ' does not exist'); 68 | } 69 | 70 | $config = $serviceLocator->get('config'); 71 | $objectManagerName = $this->getObjectManagerName(); 72 | 73 | // Copied from DoctrineModule/ServiceFactory/AbstractDoctrineServiceFactory 74 | if ( 75 | ! preg_match( 76 | '/^doctrine\.((?orm|odm)\.|)(?[a-z0-9_]+)\.(?[a-z0-9_]+)$/', 77 | $objectManagerName, 78 | $matches, 79 | ) 80 | ) { 81 | throw new RuntimeException('The object manager name is invalid: ' . $objectManagerName); 82 | } 83 | 84 | $migrationConfig = $config['doctrine']['migrations_configuration'][$matches['serviceName']] ?? []; 85 | $dependencyFactoryServices = []; 86 | 87 | if (array_key_exists('dependency_factory_services', $migrationConfig)) { 88 | $dependencyFactoryServices = $migrationConfig['dependency_factory_services']; 89 | unset($migrationConfig['dependency_factory_services']); 90 | } 91 | 92 | $dependencyFactory = DependencyFactory::fromEntityManager( 93 | new ConfigurationArray($migrationConfig), 94 | new ExistingEntityManager($serviceLocator->get($objectManagerName)), 95 | ); 96 | 97 | foreach ($dependencyFactoryServices as $id => $service) { 98 | $dependencyFactory->setService($id, $serviceLocator->get($service)); 99 | } 100 | 101 | // An object manager may not have a migrations configuration and that's OK. 102 | // Use default values in that case. 103 | return new $commandClassName($dependencyFactory); 104 | } 105 | 106 | private function getObjectManagerName(): string 107 | { 108 | $arguments = new ArgvInput(); 109 | 110 | if (! $arguments->hasParameterOption('--object-manager')) { 111 | return $this->defaultObjectManagerName; 112 | } 113 | 114 | return $arguments->getParameterOption('--object-manager'); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/CliConfigurator.php: -------------------------------------------------------------------------------- 1 | getAvailableCommands(); 69 | foreach ($commands as $commandName) { 70 | $command = $this->container->get($commandName); 71 | $command->getDefinition()->addOption($this->createObjectManagerInputOption()); 72 | 73 | $cli->add($command); 74 | } 75 | 76 | $objectManager = $this->container->get($this->getObjectManagerName()); 77 | 78 | $helpers = $this->getHelpers($objectManager); 79 | foreach ($helpers as $name => $instance) { 80 | $cli->getHelperSet()->set($instance, $name); 81 | } 82 | } 83 | 84 | /** @return array */ 85 | private function getHelpers(EntityManagerInterface $objectManager): array 86 | { 87 | return [ 88 | 'dialog' => new QuestionHelper(), 89 | 'em' => new EntityManagerHelper($objectManager), 90 | ]; 91 | } 92 | 93 | private function createObjectManagerInputOption(): InputOption 94 | { 95 | return new InputOption( 96 | 'object-manager', 97 | null, 98 | InputOption::VALUE_OPTIONAL, 99 | 'The name of the object manager to use.', 100 | $this->defaultObjectManagerName, 101 | ); 102 | } 103 | 104 | private function getObjectManagerName(): string 105 | { 106 | $arguments = new ArgvInput(); 107 | 108 | if (! $arguments->hasParameterOption('--object-manager')) { 109 | return $this->defaultObjectManagerName; 110 | } 111 | 112 | return $arguments->getParameterOption('--object-manager'); 113 | } 114 | 115 | /** @return string[] */ 116 | private function getAvailableCommands(): array 117 | { 118 | if (class_exists(VersionCommand::class)) { 119 | return ArrayUtils::merge($this->commands, $this->migrationCommands); 120 | } 121 | 122 | return $this->commands; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /docs/en/cache.rst: -------------------------------------------------------------------------------- 1 | Caching 2 | ======= 3 | 4 | Caching is very important in Doctrine. 5 | 6 | In this example for Metadata, Queries, and Results we set an array 7 | cache for the result\_cache. Please note the array cache is for 8 | development only and shown here along side other cache types. 9 | 10 | If you want to set a cache for query, result and metadata, you can 11 | specify this inside your ``config/autoload/local.php`` 12 | 13 | .. code:: php 14 | 15 | return [ 16 | 'doctrine' => [ 17 | 'configuration' => [ 18 | 'orm_default' => [ 19 | 'query_cache' => 'filesystem', 20 | 'result_cache' => 'array', 21 | 'metadata_cache' => 'apc', 22 | 'hydration_cache' => 'memcached', 23 | ], 24 | ], 25 | ], 26 | ]; 27 | 28 | The previous configuration takes into consideration different cache 29 | adapters. You can specify any other adapter that implements the 30 | ``Doctrine\Common\Cache\Cache`` interface. Find more 31 | `here `__. 32 | 33 | 34 | Redis Example 35 | ------------- 36 | 37 | This example uses a factory to create the Redis cache object. The Redis install used here 38 | can be found at `https://github.com/phpredis/phpredis#class-redis `__ 39 | See also `https://redislabs.com/lp/php-redis/ `__ 40 | 41 | module.config.php 42 | 43 | .. code:: php 44 | 45 | namespace Db; 46 | 47 | return [ 48 | 'service_manager' => [ 49 | 'factories' => [ 50 | 'Db\Cache\Redis' => Db\Cache\RedisFactory::class, 51 | ], 52 | ], 53 | 'doctrine' => [ 54 | 'cache' => [ 55 | 'redis' => [ 56 | 'namespace' => 'Db_Doctrine', 57 | 'instance' => 'Db\Cache\Redis', 58 | ], 59 | ], 60 | 'configuration' => [ 61 | 'orm_default' => [ 62 | 'query_cache' => 'redis', 63 | 'result_cache' => 'redis', 64 | 'metadata_cache' => 'redis', 65 | 'hydration_cache' => 'redis', 66 | ], 67 | ], 68 | ], 69 | ]; 70 | 71 | 72 | Db\\Cache\\RedisFactory 73 | 74 | .. code:: php 75 | 76 | namespace Db\Cache; 77 | 78 | use Psr\Container\ContainerInterface; 79 | use Redis; 80 | 81 | class RedisFactory 82 | { 83 | public function __invoke( 84 | ContainerInterface $container, 85 | $requestedName, 86 | array $options = null 87 | ) { 88 | $redis = new Redis(); 89 | $redis->connect('127.0.0.1', 6379); 90 | 91 | return $redis; 92 | } 93 | } 94 | 95 | 96 | Read more about 97 | `Caching `__. 98 | 99 | 100 | How to enable and configure Second Level Cache 101 | ---------------------------------------------- 102 | 103 | .. code:: php 104 | 105 | return [ 106 | 'doctrine' => [ 107 | 'configuration' => [ 108 | 'orm_default' => [ 109 | 'result_cache' => 'redis', // Second level cache reuse the cache defined in result cache 110 | 'second_level_cache' => [ 111 | 'enabled' => true, 112 | 'default_lifetime' => 200, 113 | 'default_lock_lifetime' => 500, 114 | 'file_lock_region_directory' => __DIR__ . '/../my_dir', 115 | 'regions' => [ 116 | 'My\FirstRegion\Name' => [ 117 | 'lifetime' => 800, 118 | 'lock_lifetime' => 1000, 119 | ], 120 | 'My\SecondRegion\Name' => [ 121 | 'lifetime' => 10, 122 | 'lock_lifetime' => 20, 123 | ], 124 | ], 125 | ], 126 | ], 127 | ], 128 | ], 129 | ]; 130 | 131 | You also need to add the ``Cache`` annotation to your model (`read 132 | more `__). 133 | Read more about `Second Level 134 | Cache `__. 135 | -------------------------------------------------------------------------------- /docs/en/user-guide.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | Installation of this module uses composer. For composer documentation, 5 | please refer to `getcomposer.org `__. 6 | 7 | .. code:: sh 8 | 9 | composer require doctrine/doctrine-orm-module 10 | 11 | Then add ``DoctrineModule`` and ``DoctrineORMModule`` to your 12 | ``config/application.config.php`` and create directory 13 | ``data/DoctrineORMModule/Proxy`` and make sure your application has 14 | write access to it. 15 | 16 | Installation without composer is not officially supported and requires 17 | you to manually install all dependencies that are listed in 18 | ``composer.json`` 19 | 20 | Entities settings 21 | ----------------- 22 | 23 | To register your entities with the ORM, add following metadata driver 24 | configurations to your module (merged) configuration for each of your 25 | entities namespaces: 26 | 27 | .. code:: php 28 | 29 | [ 32 | 'driver' => [ 33 | // defines an annotation driver with two paths, and names it `my_annotation_driver` 34 | 'my_annotation_driver' => [ 35 | 'class' => \Doctrine\ORM\Mapping\Driver\AnnotationDriver::class, 36 | 'cache' => 'array', 37 | 'paths' => [ 38 | 'path/to/my/entities', 39 | 'another/path', 40 | ], 41 | ], 42 | 43 | // default metadata driver, aggregates all other drivers into a single one. 44 | // Override `orm_default` only if you know what you're doing 45 | 'orm_default' => [ 46 | 'drivers' => [ 47 | // register `my_annotation_driver` for any entity under namespace `My\Namespace` 48 | 'My\Namespace' => 'my_annotation_driver', 49 | ], 50 | ], 51 | ], 52 | ], 53 | ]; 54 | 55 | Connection settings 56 | ------------------- 57 | 58 | Connection parameters can be defined in the application configuration: 59 | 60 | .. code:: php 61 | 62 | [ 65 | 'connection' => [ 66 | // default connection name 67 | 'orm_default' => [ 68 | 'driverClass' => \Doctrine\DBAL\Driver\PDO\MySQL\Driver::class, 69 | 'params' => [ 70 | 'host' => 'localhost', 71 | 'port' => '3306', 72 | 'user' => 'username', 73 | 'password' => 'password', 74 | 'dbname' => 'database', 75 | ], 76 | ], 77 | ], 78 | ], 79 | ]; 80 | 81 | Full configuration options 82 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 83 | 84 | An exhaustive list of configuration options can be found directly in the 85 | Options classes of each module. 86 | 87 | - `DoctrineModule 88 | configuration `__ 89 | - `ORM Module 90 | Configuration `__ 91 | - `ORM Module 92 | Defaults `__ 93 | 94 | You can find documentation about the module’s features at the following 95 | links: 96 | 97 | - `DoctrineModule 98 | documentation `__ 99 | - `DoctrineORMModule 100 | documentation `__ 101 | 102 | Registered Service names 103 | ------------------------ 104 | 105 | - ``doctrine.connection.orm_default``: a ``Doctrine\DBAL\Connection`` 106 | instance 107 | - ``doctrine.configuration.orm_default``: a 108 | ``Doctrine\ORM\Configuration`` instance 109 | - ``doctrine.driver.orm_default``: default mapping driver instance 110 | - ``doctrine.entitymanager.orm_default``: the 111 | ``Doctrine\ORM\EntityManager`` instance 112 | - ``Doctrine\ORM\EntityManager``: an alias of 113 | ``doctrine.entitymanager.orm_default`` 114 | - ``doctrine.eventmanager.orm_default``: the 115 | ``Doctrine\Common\EventManager`` instance 116 | 117 | Command Line 118 | ^^^^^^^^^^^^ 119 | 120 | Access the Doctrine command line as following 121 | 122 | .. code:: sh 123 | 124 | ./vendor/bin/doctrine-module 125 | 126 | Service Locator 127 | ^^^^^^^^^^^^^^^ 128 | 129 | To access the entity manager, use the main service locator: 130 | 131 | .. code:: php 132 | 133 | // for example, in a controller: 134 | $em = $this->getServiceLocator()->get('doctrine.entitymanager.orm_default'); 135 | $em = $this->getServiceLocator()->get(\Doctrine\ORM\EntityManager::class); 136 | -------------------------------------------------------------------------------- /view/laminas-developer-tools/toolbar/doctrine-orm-queries.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Doctrine ORM (Queries) 5 | 6 | getQueryCount(); ?> 7 | queries in 8 | LaminasDeveloperToolsTime($collector->getQueryTime()); ?> 9 | 10 |
11 |
12 | 13 | 14 | DoctrineORMModule 15 | 16 | 17 | 18 | Queries for 19 | getName() ?> 20 | 21 | 22 | getQueries() as $query): ?> 23 |
24 | SQL 25 | 26 | 27 | $1', $this->escapeHtml($query['sql'])) ?> 28 |
29 | 30 | Params 31 | 32 | 33 | $value): ?> 34 |      =>
35 | 36 | 37 |
38 | 39 | Types 40 | 41 | 42 | $value): ?> 43 |      =>
44 | 45 | 46 |
47 | 48 | Time 49 | 50 | 51 | 52 | 53 |
54 |
55 | 85 | -------------------------------------------------------------------------------- /src/Options/DBALConnection.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class DBALConnection extends AbstractOptions 19 | { 20 | /** 21 | * Set the configuration key for the Configuration. Configuration key 22 | * is assembled as "doctrine.configuration.{key}" and pulled from 23 | * service locator. 24 | */ 25 | protected string $configuration = 'orm_default'; 26 | 27 | /** 28 | * Set the eventmanager key for the EventManager. EventManager key 29 | * is assembled as "doctrine.eventmanager.{key}" and pulled from 30 | * service locator. 31 | */ 32 | protected string $eventmanager = 'orm_default'; 33 | 34 | /** 35 | * Set the PDO instance, if any, to use. If a string is set 36 | * then the alias is pulled from the service locator. 37 | */ 38 | protected string|PDO|null $pdo = null; 39 | 40 | /** 41 | * Setting the driver is deprecated. You should set the 42 | * driver class directly instead. 43 | */ 44 | protected string $driverClass = PDOMySQLDriver::class; 45 | 46 | /** 47 | * Set the wrapper class for the driver. In general, this should not 48 | * need to be changed. 49 | */ 50 | protected string|null $wrapperClass = null; 51 | 52 | /** 53 | * Driver specific connection parameters. 54 | * 55 | * @var mixed[] 56 | */ 57 | protected array $params = []; 58 | 59 | /** @var mixed[] */ 60 | protected array $doctrineTypeMappings = []; 61 | 62 | /** @var mixed[] */ 63 | protected array $doctrineCommentedTypes = []; 64 | 65 | protected bool $useSavepoints = false; 66 | 67 | public function setConfiguration(string $configuration): void 68 | { 69 | $this->configuration = $configuration; 70 | } 71 | 72 | public function getConfiguration(): string 73 | { 74 | return 'doctrine.configuration.' . $this->configuration; 75 | } 76 | 77 | public function setEventmanager(string $eventmanager): void 78 | { 79 | $this->eventmanager = $eventmanager; 80 | } 81 | 82 | public function getEventmanager(): string 83 | { 84 | return 'doctrine.eventmanager.' . $this->eventmanager; 85 | } 86 | 87 | /** @param mixed[] $params */ 88 | public function setParams(array $params): void 89 | { 90 | $this->params = $params; 91 | } 92 | 93 | /** @return mixed[] */ 94 | public function getParams(): array 95 | { 96 | return $this->params; 97 | } 98 | 99 | /** @param mixed[] $doctrineTypeMappings */ 100 | public function setDoctrineTypeMappings(array $doctrineTypeMappings): DBALConnection 101 | { 102 | $this->doctrineTypeMappings = $doctrineTypeMappings; 103 | 104 | return $this; 105 | } 106 | 107 | /** @return mixed[] */ 108 | public function getDoctrineTypeMappings(): array 109 | { 110 | return $this->doctrineTypeMappings; 111 | } 112 | 113 | /** 114 | * @deprecated 5.1.0 Deprecated in DBAL 3.3, use `Type::requiresSQLCommentTypeHint()` instead. 115 | * 116 | * @param mixed[] $doctrineCommentedTypes 117 | */ 118 | public function setDoctrineCommentedTypes(array $doctrineCommentedTypes): void 119 | { 120 | $this->doctrineCommentedTypes = $doctrineCommentedTypes; 121 | } 122 | 123 | /** 124 | * @deprecated 5.1.0 Deprecated in DBAL 3.3, use `Type::requiresSQLCommentTypeHint()` instead. 125 | * 126 | * @return mixed[] 127 | */ 128 | public function getDoctrineCommentedTypes(): array 129 | { 130 | return $this->doctrineCommentedTypes; 131 | } 132 | 133 | public function setDriverClass(string|null $driverClass): void 134 | { 135 | $this->driverClass = $driverClass; 136 | } 137 | 138 | /** @return class-string|null */ 139 | public function getDriverClass(): string|null 140 | { 141 | return $this->driverClass; 142 | } 143 | 144 | public function setPdo(PDO|string|null $pdo): void 145 | { 146 | $this->pdo = $pdo; 147 | } 148 | 149 | public function getPdo(): PDO|string|null 150 | { 151 | return $this->pdo; 152 | } 153 | 154 | public function setWrapperClass(string $wrapperClass): void 155 | { 156 | $this->wrapperClass = $wrapperClass; 157 | } 158 | 159 | /** @return class-string|null */ 160 | public function getWrapperClass(): string|null 161 | { 162 | return $this->wrapperClass; 163 | } 164 | 165 | public function useSavepoints(): bool 166 | { 167 | return $this->useSavepoints; 168 | } 169 | 170 | public function setUseSavepoints(bool $useSavepoints): void 171 | { 172 | $this->useSavepoints = $useSavepoints; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /docs/en/developer-tools.rst: -------------------------------------------------------------------------------- 1 | Laminas Developer Tools in DoctrineORMModule 2 | ============================================ 3 | 4 | If you ever tried `Laminas Developer 5 | Tools `__ you will 6 | surely understand the importance of being able to track performance 7 | pitfalls or excessive amount of queries in your applications when 8 | developing. 9 | 10 | Setup 11 | ----- 12 | 13 | To setup `Laminas Developer 14 | Tools `__, run 15 | 16 | .. code:: sh 17 | 18 | composer require laminas/laminas-developer-tools 19 | 20 | Then enable ``Laminas\DeveloperTools`` in your modules and enable profiling 21 | and the toolbar (see docs of Laminas Developer Tools for that). 22 | 23 | Once ``Laminas\DeveloperTools`` is enabled, having 24 | ``doctrine.entity_manager.orm_default`` as your default 25 | ``EntityManager``, you will notice that the queries performed by the ORM 26 | get logged and displayed in the toolbar. 27 | 28 | .. figure:: https://github.com/doctrine/DoctrineORMModule/raw/4.0.x/docs/images/laminas-developer-tools-doctrine-module.png 29 | 30 | Customization 31 | ------------- 32 | 33 | If you want to customize this behavior (or track multiple 34 | ``EntityManager`` instances) you can do it in different ways. Please 35 | note that if you have set an ``SQLLogger`` in your configuration, this 36 | functionality won't override it, so you can use these features in total 37 | safety. 38 | 39 | Multiple EntityManager/Connection instances and logging 40 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | 42 | *WARNING! These are advanced features! Even if the code is fully tested, 43 | this is usually not required for most users!* 44 | 45 | To setup logging for an additional DBAL Connection or EntityManager, put 46 | something like following in your module: 47 | 48 | .. code:: php 49 | 50 | [ 60 | 'sql_logger_collector' => [ 61 | 'other_orm' => [ 62 | // name of the sql logger collector (used by Laminas\DeveloperTools) 63 | 'name' => 'other_orm', 64 | 65 | // name of the configuration service at which to attach the logger 66 | 'configuration' => 'doctrine.configuration.other_orm', 67 | 68 | // uncomment following if you want to use a particular SQL logger instead of relying on 69 | // the attached one 70 | //'sql_logger' => 'service_name_of_my_dbal_sql_logger', 71 | ], 72 | ], 73 | ], 74 | 75 | 'laminas-developer-tools' => [ 76 | 77 | // registering the profiler with Laminas\DeveloperTools 78 | 'profiler' => [ 79 | 'collectors' => [ 80 | // reference to the service we have defined 81 | 'other_orm' => 'doctrine.sql_logger_collector.other_orm', 82 | ], 83 | ], 84 | 85 | // registering a new toolbar item with Laminas\DeveloperTools (name must be the same of the collector name) 86 | 'toolbar' => [ 87 | 'entries' => [ 88 | // this is actually a name of a view script to use - you can use your custom one 89 | 'other_orm' => 'laminas-developer-tools/toolbar/doctrine-orm', 90 | ], 91 | ], 92 | ], 93 | ]; 94 | } 95 | 96 | public function getServiceConfiguration() 97 | { 98 | return [ 99 | 'factories' => [ 100 | // defining a service (any name is valid as long as you use it consistently across this example) 101 | 'doctrine.sql_logger_collector.other_orm' => new \DoctrineORMModule\Service\SQLLoggerCollectorFactory('other_orm'), 102 | ], 103 | ]; 104 | } 105 | 106 | public function onBootstrap(\Laminas\EventManager\EventInterface $e) 107 | { 108 | $config = $e->getTarget()->getServiceManager()->get('Config'); 109 | 110 | if (isset($config['laminas-developer-tools']['profiler']['enabled']) 111 | && $config['laminas-developer-tools']['profiler']['enabled'] 112 | ) { 113 | // when Laminas\DeveloperTools is enabled, initialize the sql collector 114 | $app->getServiceManager()->get('doctrine.sql_logger_collector.other_orm'); 115 | } 116 | } 117 | } 118 | 119 | This example will simply generate a new icon in the toolbar, with the 120 | log results of your ``other_orm`` connection: 121 | 122 | .. figure:: https://github.com/doctrine/DoctrineORMModule/raw/4.0.x/docs/images/laminas-developer-tools-multiple-entity-managers.png 123 | -------------------------------------------------------------------------------- /src/Form/Annotation/EntityBasedFormBuilder.php: -------------------------------------------------------------------------------- 1 | builder = $builder ?? new LaminasAnnotationBuilder(); 50 | (new DoctrineAnnotationListener($this->objectManager))->attach($this->builder->getEventManager()); 51 | } 52 | 53 | /** @return AbstractBuilder the form builder from laminas-form */ 54 | public function getBuilder(): AbstractBuilder 55 | { 56 | return $this->builder; 57 | } 58 | 59 | /** 60 | * Overrides the base getFormSpecification() to additionally iterate through each 61 | * field/association in the metadata and trigger the associated event. 62 | * 63 | * This allows building of a form from metadata instead of requiring annotations. 64 | * Annotations are still allowed through the ElementAnnotationsListener. 65 | * 66 | * @param class-string|object $entity Either an instance or a valid class name for an entity 67 | * 68 | * @throws InvalidArgumentException If $entity is not an object or class name. 69 | */ 70 | public function getFormSpecification(string|object $entity): ArrayObject 71 | { 72 | $formSpec = $this->getBuilder()->getFormSpecification($entity); 73 | $metadata = $this->objectManager->getClassMetadata(is_object($entity) ? $entity::class : $entity); 74 | $inputFilter = $formSpec['input_filter']; 75 | 76 | $formElements = [ 77 | EntitySelect::class, 78 | EntityMultiCheckbox::class, 79 | EntityRadio::class, 80 | ObjectSelect::class, 81 | ObjectMultiCheckbox::class, 82 | ObjectRadio::class, 83 | ]; 84 | 85 | foreach ($formSpec['elements'] as $key => $elementSpec) { 86 | $name = $elementSpec['spec']['name'] ?? null; 87 | $isFormElement = (isset($elementSpec['spec']['type']) && 88 | in_array($elementSpec['spec']['type'], $formElements)); 89 | 90 | if (! $name) { 91 | continue; 92 | } 93 | 94 | if (! isset($inputFilter[$name])) { 95 | $inputFilter[$name] = new ArrayObject(); 96 | } 97 | 98 | $params = [ 99 | 'metadata' => $metadata, 100 | 'name' => $name, 101 | 'elementSpec' => $elementSpec, 102 | 'inputSpec' => $inputFilter[$name], 103 | ]; 104 | 105 | if ($this->checkForExcludeElementFromMetadata($metadata, $name)) { 106 | $elementSpec = $formSpec['elements']; 107 | unset($elementSpec[$key]); 108 | $formSpec['elements'] = $elementSpec; 109 | 110 | if (isset($inputFilter[$name])) { 111 | unset($inputFilter[$name]); 112 | } 113 | 114 | $formSpec['input_filter'] = $inputFilter; 115 | continue; 116 | } 117 | 118 | if ($metadata->hasField($name) || (! $metadata->hasAssociation($name) && $isFormElement)) { 119 | $this->getBuilder()->getEventManager()->trigger(self::EVENT_CONFIGURE_FIELD, $this, $params); 120 | } elseif ($metadata->hasAssociation($name)) { 121 | $this->getBuilder()->getEventManager()->trigger(self::EVENT_CONFIGURE_ASSOCIATION, $this, $params); 122 | } 123 | } 124 | 125 | $formSpec['options'] = ['prefer_form_input_filter' => true]; 126 | 127 | return $formSpec; 128 | } 129 | 130 | /** 131 | * Create a form from an object. 132 | * 133 | * @param class-string|object $entity 134 | */ 135 | public function createForm(string|object $entity): FormInterface 136 | { 137 | $formSpec = ArrayUtils::iteratorToArray($this->getFormSpecification($entity)); 138 | $formFactory = $this->getBuilder()->getFormFactory(); 139 | 140 | return $formFactory->createForm($formSpec); 141 | } 142 | 143 | private function checkForExcludeElementFromMetadata(ClassMetadata $metadata, string $name): bool 144 | { 145 | $params = ['metadata' => $metadata, 'name' => $name]; 146 | $result = false; 147 | 148 | if ($metadata->hasField($name)) { 149 | $result = $this->getBuilder()->getEventManager()->trigger(self::EVENT_EXCLUDE_FIELD, $this, $params); 150 | } elseif ($metadata->hasAssociation($name)) { 151 | $result = $this->getBuilder()->getEventManager()->trigger(self::EVENT_EXCLUDE_ASSOCIATION, $this, $params); 152 | } 153 | 154 | if ($result) { 155 | $result = (bool) $result->last(); 156 | } 157 | 158 | return $result; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Service/ConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | getOptions($serviceLocator); 33 | $config = new Configuration(); 34 | 35 | $config->setAutoGenerateProxyClasses($options->getGenerateProxies()); 36 | $config->setProxyDir($options->getProxyDir()); 37 | $config->setProxyNamespace($options->getProxyNamespace()); 38 | 39 | $config->setEntityNamespaces($options->getEntityNamespaces()); 40 | 41 | $config->setCustomDatetimeFunctions($options->getDatetimeFunctions()); 42 | $config->setCustomStringFunctions($options->getStringFunctions()); 43 | $config->setCustomNumericFunctions($options->getNumericFunctions()); 44 | 45 | $config->setClassMetadataFactoryName($options->getClassMetadataFactoryName()); 46 | 47 | foreach ($options->getNamedQueries() as $name => $query) { 48 | $config->addNamedQuery($name, $query); 49 | } 50 | 51 | foreach ($options->getNamedNativeQueries() as $name => $query) { 52 | $config->addNamedNativeQuery($name, $query['sql'], new $query['rsm']()); 53 | } 54 | 55 | foreach ($options->getCustomHydrationModes() as $modeName => $hydrator) { 56 | $config->addCustomHydrationMode($modeName, $hydrator); 57 | } 58 | 59 | foreach ($options->getFilters() as $name => $class) { 60 | $config->addFilter($name, $class); 61 | } 62 | 63 | $config->setMetadataCacheImpl($serviceLocator->get($options->getMetadataCache())); 64 | $config->setQueryCacheImpl($serviceLocator->get($options->getQueryCache())); 65 | $config->setResultCacheImpl($serviceLocator->get($options->getResultCache())); 66 | $config->setHydrationCacheImpl($serviceLocator->get($options->getHydrationCache())); 67 | $config->setMetadataDriverImpl($serviceLocator->get($options->getDriver())); 68 | 69 | $namingStrategy = $options->getNamingStrategy(); 70 | if ($namingStrategy) { 71 | if (is_string($namingStrategy)) { 72 | if (! $serviceLocator->has($namingStrategy)) { 73 | throw new InvalidArgumentException(sprintf('Naming strategy "%s" not found', $namingStrategy)); 74 | } 75 | 76 | $config->setNamingStrategy($serviceLocator->get($namingStrategy)); 77 | } else { 78 | $config->setNamingStrategy($namingStrategy); 79 | } 80 | } 81 | 82 | $quoteStrategy = $options->getQuoteStrategy(); 83 | if ($quoteStrategy) { 84 | if (is_string($quoteStrategy)) { 85 | if (! $serviceLocator->has($quoteStrategy)) { 86 | throw new InvalidArgumentException(sprintf('Quote strategy "%s" not found', $quoteStrategy)); 87 | } 88 | 89 | $config->setQuoteStrategy($serviceLocator->get($quoteStrategy)); 90 | } else { 91 | $config->setQuoteStrategy($quoteStrategy); 92 | } 93 | } 94 | 95 | $repositoryFactory = $options->getRepositoryFactory(); 96 | if ($repositoryFactory) { 97 | if (is_string($repositoryFactory)) { 98 | if (! $serviceLocator->has($repositoryFactory)) { 99 | throw new InvalidArgumentException( 100 | sprintf('Repository factory "%s" not found', $repositoryFactory), 101 | ); 102 | } 103 | 104 | $config->setRepositoryFactory($serviceLocator->get($repositoryFactory)); 105 | } else { 106 | $config->setRepositoryFactory($repositoryFactory); 107 | } 108 | } 109 | 110 | $entityListenerResolver = $options->getEntityListenerResolver(); 111 | if ($entityListenerResolver) { 112 | if ($entityListenerResolver instanceof EntityListenerResolver) { 113 | $config->setEntityListenerResolver($entityListenerResolver); 114 | } else { 115 | $config->setEntityListenerResolver($serviceLocator->get($entityListenerResolver)); 116 | } 117 | } 118 | 119 | $secondLevelCache = $options->getSecondLevelCache(); 120 | 121 | if ($secondLevelCache->isEnabled()) { 122 | $regionsConfig = new RegionsConfiguration( 123 | $secondLevelCache->getDefaultLifetime(), 124 | $secondLevelCache->getDefaultLockLifetime(), 125 | ); 126 | 127 | foreach ($secondLevelCache->getRegions() as $regionName => $regionConfig) { 128 | if (isset($regionConfig['lifetime'])) { 129 | $regionsConfig->setLifetime($regionName, $regionConfig['lifetime']); 130 | } 131 | 132 | if (! isset($regionConfig['lock_lifetime'])) { 133 | continue; 134 | } 135 | 136 | $regionsConfig->setLockLifetime($regionName, $regionConfig['lock_lifetime']); 137 | } 138 | 139 | // As Second Level Cache caches queries results, we reuse the result cache impl 140 | $cacheFactory = new DefaultCacheFactory($regionsConfig, $config->getResultCache()); 141 | $cacheFactory->setFileLockRegionDirectory($secondLevelCache->getFileLockRegionDirectory()); 142 | 143 | $cacheConfiguration = new CacheConfiguration(); 144 | $cacheConfiguration->setCacheFactory($cacheFactory); 145 | $cacheConfiguration->setRegionsConfiguration($regionsConfig); 146 | 147 | $config->setSecondLevelCacheEnabled(); 148 | $config->setSecondLevelCacheConfiguration($cacheConfiguration); 149 | } 150 | 151 | // only works for DBAL 2.x, not for 3.x 152 | if (method_exists($config, 'setFilterSchemaAssetsExpression')) { 153 | $filterSchemaAssetsExpression = $options->getFilterSchemaAssetsExpression(); 154 | if ($filterSchemaAssetsExpression) { 155 | $config->setFilterSchemaAssetsExpression($filterSchemaAssetsExpression); 156 | } 157 | } 158 | 159 | // DBAL 2.x 160 | if (method_exists($config, 'setSchemaAssetsFilter')) { 161 | $schemaAssetsFilter = $options->getSchemaAssetsFilter(); 162 | if ($schemaAssetsFilter) { 163 | $config->setSchemaAssetsFilter($schemaAssetsFilter); 164 | } 165 | } 166 | 167 | $className = $options->getDefaultRepositoryClassName(); 168 | if ($className) { 169 | $config->setDefaultRepositoryClassName($className); 170 | } 171 | 172 | $this->setupDBALConfiguration($serviceLocator, $config); 173 | 174 | return $config; 175 | } 176 | 177 | protected function getOptionsClass(): string 178 | { 179 | return DoctrineORMModuleConfiguration::class; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Yuml/MetadataGrapher.php: -------------------------------------------------------------------------------- 1 | > 25 | */ 26 | protected array $visitedAssociations = []; 27 | 28 | /** @var ClassMetadata[] */ 29 | private array $metadata; 30 | 31 | /** 32 | * Temporary array where reverse association name are stored 33 | * 34 | * @var ClassMetadata[] 35 | */ 36 | private array $classByNames = []; 37 | 38 | /** 39 | * Generate a YUML compatible `dsl_text` to describe a given array 40 | * of entities 41 | * 42 | * @param ClassMetadata[] $metadata 43 | */ 44 | public function generateFromMetadata(array $metadata): string 45 | { 46 | $this->metadata = $metadata; 47 | $this->visitedAssociations = []; 48 | $str = []; 49 | 50 | foreach ($metadata as $class) { 51 | $parent = $this->getParent($class); 52 | 53 | if ($parent) { 54 | $str[] = $this->getClassString($parent) . '^' . $this->getClassString($class); 55 | } 56 | 57 | $associations = $class->getAssociationNames(); 58 | 59 | if (empty($associations) && ! isset($this->visitedAssociations[$class->getName()])) { 60 | $str[] = $this->getClassString($class); 61 | 62 | continue; 63 | } 64 | 65 | foreach ($associations as $associationName) { 66 | if ($parent && in_array($associationName, $parent->getAssociationNames())) { 67 | continue; 68 | } 69 | 70 | if (! $this->visitAssociation($class->getName(), $associationName)) { 71 | continue; 72 | } 73 | 74 | $str[] = $this->getAssociationString($class, $associationName); 75 | } 76 | } 77 | 78 | return implode(',', $str); 79 | } 80 | 81 | private function getAssociationString(ClassMetadata $class1, string $association): string 82 | { 83 | $targetClassName = $class1->getAssociationTargetClass($association); 84 | $class2 = $this->getClassByName($targetClassName); 85 | $isInverse = $class1->isAssociationInverseSide($association); 86 | $class1Count = $class1->isCollectionValuedAssociation($association) ? 2 : 1; 87 | 88 | if ($class2 === null) { 89 | return $this->getClassString($class1) 90 | . ($isInverse ? '<' : '<>') . '-' . $association . ' ' 91 | . ($class1Count > 1 ? '*' : '1') 92 | . ($isInverse ? '<>' : '>') 93 | . '[' . str_replace('\\', '.', $targetClassName) . ']'; 94 | } 95 | 96 | $class1SideName = $association; 97 | $class2SideName = $this->getClassReverseAssociationName($class1, $class2); 98 | $class2Count = 0; 99 | $bidirectional = false; 100 | 101 | if ($class2SideName !== null) { 102 | if ($isInverse) { 103 | $class2Count = $class2->isCollectionValuedAssociation($class2SideName) ? 2 : 1; 104 | $bidirectional = true; 105 | } elseif ($class2->isAssociationInverseSide($class2SideName)) { 106 | $class2Count = $class2->isCollectionValuedAssociation($class2SideName) ? 2 : 1; 107 | $bidirectional = true; 108 | } 109 | } 110 | 111 | $this->visitAssociation($targetClassName, $class2SideName); 112 | 113 | return $this->getClassString($class1) 114 | . ($bidirectional ? ($isInverse ? '<' : '<>') : '') // class2 side arrow 115 | . ($class2SideName ? $class2SideName . ' ' : '') 116 | . ($class2Count > 1 ? '*' : ($class2Count ? '1' : '')) // class2 side single/multi valued 117 | . '-' 118 | . $class1SideName . ' ' 119 | . ($class1Count > 1 ? '*' : '1') // class1 side single/multi valued 120 | . ($bidirectional && $isInverse ? '<>' : '>') // class1 side arrow 121 | . $this->getClassString($class2); 122 | } 123 | 124 | private function getClassReverseAssociationName(ClassMetadata $class1, ClassMetadata $class2): string|null 125 | { 126 | foreach ($class2->getAssociationNames() as $class2Side) { 127 | $targetClass = $this->getClassByName($class2->getAssociationTargetClass($class2Side)); 128 | if (! $targetClass) { 129 | throw new Exception('Invalid class name for AssociationTargetClass ' . $class2Side); 130 | } 131 | 132 | if ($class1->getName() === $targetClass->getName()) { 133 | return $class2Side; 134 | } 135 | } 136 | 137 | return null; 138 | } 139 | 140 | /** 141 | * Build the string representing the single graph item 142 | */ 143 | private function getClassString(ClassMetadata $class): string 144 | { 145 | $this->visitAssociation($class->getName()); 146 | 147 | $className = $class->getName(); 148 | $classText = '[' . str_replace('\\', '.', $className); 149 | $fields = []; 150 | $parent = $this->getParent($class); 151 | $parentFields = $parent ? $parent->getFieldNames() : []; 152 | 153 | foreach ($class->getFieldNames() as $fieldName) { 154 | if (in_array($fieldName, $parentFields)) { 155 | continue; 156 | } 157 | 158 | if ($class->isIdentifier($fieldName)) { 159 | $fields[] = '+' . $fieldName; 160 | } else { 161 | $fields[] = $fieldName; 162 | } 163 | } 164 | 165 | if (! empty($fields)) { 166 | $classText .= '|' . implode(';', $fields); 167 | } 168 | 169 | $classText .= ']'; 170 | 171 | return $classText; 172 | } 173 | 174 | /** 175 | * Retrieve a class metadata instance by name from the given array 176 | */ 177 | private function getClassByName(string $className): ClassMetadata|null 178 | { 179 | if (! isset($this->classByNames[$className])) { 180 | foreach ($this->metadata as $class) { 181 | if ($class->getName() === $className) { 182 | $this->classByNames[$className] = $class; 183 | break; 184 | } 185 | } 186 | } 187 | 188 | return $this->classByNames[$className] ?? null; 189 | } 190 | 191 | /** 192 | * Retrieve a class metadata's parent class metadata 193 | */ 194 | private function getParent(ClassMetadata $class): ClassMetadata|null 195 | { 196 | $className = $class->getName(); 197 | if (! class_exists($className)) { 198 | return null; 199 | } 200 | 201 | $parent = get_parent_class($className); 202 | if (! class_exists($className) || ! $parent) { 203 | return null; 204 | } 205 | 206 | return $this->getClassByName($parent); 207 | } 208 | 209 | /** 210 | * Visit a given association and mark it as visited 211 | * 212 | * @phpstan-param class-string $className 213 | * 214 | * @return bool true if the association was visited before 215 | */ 216 | private function visitAssociation(string $className, string|null $association = null): bool 217 | { 218 | if ($association === null) { 219 | if (isset($this->visitedAssociations[$className])) { 220 | return false; 221 | } 222 | 223 | $this->visitedAssociations[$className] = []; 224 | 225 | return true; 226 | } 227 | 228 | if (isset($this->visitedAssociations[$className][$association])) { 229 | return false; 230 | } 231 | 232 | if (! isset($this->visitedAssociations[$className])) { 233 | $this->visitedAssociations[$className] = []; 234 | } 235 | 236 | $this->visitedAssociations[$className][$association] = true; 237 | 238 | return true; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /docs/en/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Register a Custom DQL Function 5 | ------------------------------ 6 | 7 | .. code:: php 8 | 9 | return [ 10 | 'doctrine' => [ 11 | 'configuration' => [ 12 | 'orm_default' => [ 13 | 'numeric_functions' => [ 14 | 'ROUND' => \My\DoctrineExtensions\Query\Mysql\Round::class, 15 | ], 16 | ], 17 | ], 18 | ], 19 | ]; 20 | 21 | How to add a Custom Type 22 | ------------------------ 23 | 24 | First, implement a new type by extending `Doctrine\DBAL\Types\Type`. An example can be found in 25 | the `ORM cookbook `__ 26 | Then, register your type implementation with DBAL as follows: 27 | 28 | .. code:: php 29 | 30 | return [ 31 | 'doctrine' => [ 32 | 'configuration' => [ 33 | 'orm_default' => [ 34 | 'types' => [ 35 | 'newtype' => \My\Types\NewType::class, 36 | ], 37 | ], 38 | ], 39 | ], 40 | ]; 41 | 42 | .. note:: 43 | 44 | If your type uses a database type which is already `mapped by Doctrine `__, 45 | Doctrine will need a comment hint to distinguish your type from other types. In your type class, override 46 | `requiresSQLCommentHint()` to return `true` to let Doctrine add a comment hint. 47 | 48 | Next, you will need to register your custom type with the underlying database platform: 49 | 50 | .. code:: php 51 | 52 | return [ 53 | 'doctrine' => [ 54 | 'connection' => [ 55 | 'orm_default' => [ 56 | 'doctrine_type_mappings' => [ 57 | 'mytype' => 'mytype', 58 | ], 59 | ], 60 | ], 61 | ], 62 | ]; 63 | 64 | Using DBAL Middlewares 65 | ---------------------- 66 | 67 | .. note:: 68 | 69 | This feature is only available when using DBAL 3.x and has no effect on DBAL 2.x! 70 | 71 | `Official documentation `__ 72 | 73 | Laminas configuration 74 | 75 | .. code:: php 76 | 77 | return [ 78 | 'service_manager' => [ 79 | 'invokables' => [ 80 | \My\Middlewares\CustomMiddleware::class => \My\Middlewares\CustomMiddleware::class, 81 | \My\Middlewares\AnotherCustomMiddleware::class => \My\Middlewares\AnotherCustomMiddleware::class, 82 | ], 83 | ], 84 | 'doctrine' => [ 85 | 'configuration' => [ 86 | 'test_default' => [ 87 | 'middlewares' => [ 88 | \My\Middlewares\CustomMiddleware::class, 89 | \My\Middlewares\AnotherCustomMiddleware::class, 90 | ], 91 | ], 92 | ], 93 | ], 94 | ]; 95 | 96 | Built-in Resolver 97 | ----------------- 98 | 99 | How to Define Relationships with Abstract Classes and Interfaces (ResolveTargetEntityListener) 100 | 101 | .. code:: php 102 | 103 | return [ 104 | 'doctrine' => [ 105 | 'entity_resolver' => [ 106 | 'orm_default' => [ 107 | 'resolvers' => [ 108 | \Acme\InvoiceModule\Model\InvoiceSubjectInterface::class, 109 | \Acme\CustomerModule\Entity\Customer::class, 110 | ], 111 | ], 112 | ], 113 | ], 114 | ]; 115 | 116 | 117 | Set a Custom Default Repository 118 | ------------------------------- 119 | 120 | .. code:: php 121 | 122 | return [ 123 | 'doctrine' => [ 124 | 'configuration' => [ 125 | 'orm_default' => [ 126 | 'default_repository_class_name' => 'MyCustomRepository', 127 | ], 128 | ], 129 | ], 130 | ]; 131 | 132 | 133 | How to Use Two Connections 134 | -------------------------- 135 | 136 | In this example we create an 'orm_crawler' ORM connection. 137 | See also `this blog article `__. 138 | 139 | .. code:: php 140 | 141 | return [ 142 | 'doctrine' => [ 143 | 'connection' => [ 144 | 'orm_crawler' => [ 145 | 'driverClass' => \Doctrine\DBAL\Driver\PDO\MySQL\Driver::class, 146 | 'eventmanager' => 'orm_crawler', 147 | 'configuration' => 'orm_crawler', 148 | 'params' => [ 149 | 'host' => 'localhost', 150 | 'port' => '3306', 151 | 'user' => 'root', 152 | 'password' => 'root', 153 | 'dbname' => 'crawler', 154 | 'driverOptions' => [ 155 | 1002 => 'SET NAMES utf8', 156 | ], 157 | ], 158 | ], 159 | ], 160 | 161 | 'configuration' => [ 162 | 'orm_crawler' => [ 163 | 'metadata_cache' => 'array', 164 | 'query_cache' => 'array', 165 | 'result_cache' => 'array', 166 | 'hydration_cache' => 'array', 167 | 'driver' => 'orm_crawler_chain', 168 | 'generate_proxies' => true, 169 | 'proxy_dir' => 'data/DoctrineORMModule/Proxy', 170 | 'proxy_namespace' => 'DoctrineORMModule\Proxy', 171 | 'filters' => [], 172 | ], 173 | ], 174 | 175 | 'driver' => [ 176 | 'orm_crawler_annotation' => [ 177 | 'class' => \Doctrine\ORM\Mapping\Driver\AnnotationDriver::class, 178 | 'cache' => 'array', 179 | 'paths' => [ 180 | __DIR__ . '/../src/Crawler/Entity', 181 | ], 182 | ], 183 | 'orm_crawler_chain' => [ 184 | 'class' => \Doctrine\ORM\Mapping\Driver\DriverChain::class, 185 | 'drivers' => [ 186 | 'Crawler\Entity' => 'orm_crawler_annotation', 187 | ], 188 | ], 189 | ], 190 | 191 | 'entitymanager' => [ 192 | 'orm_crawler' => [ 193 | 'connection' => 'orm_crawler', 194 | 'configuration' => 'orm_crawler', 195 | ], 196 | ], 197 | 198 | 'eventmanager' => [ 199 | 'orm_crawler' => [], 200 | ], 201 | 202 | 'sql_logger_collector' => [ 203 | 'orm_crawler' => [], 204 | ], 205 | 206 | 'entity_resolver' => [ 207 | 'orm_crawler' => [], 208 | ], 209 | ], 210 | ]; 211 | 212 | The ``DoctrineModule\ServiceFactory\AbstractDoctrineServiceFactory`` will create the following objects as needed: 213 | 214 | * doctrine.connection.orm_crawler 215 | * doctrine.configuration.orm_crawler 216 | * doctrine.entitymanager.orm_crawler 217 | * doctrine.driver.orm_crawler 218 | * doctrine.eventmanager.orm_crawler 219 | * doctrine.entity_resolver.orm_crawler 220 | * doctrine.sql_logger_collector.orm_crawler 221 | 222 | 223 | You can retrieve them from the service manager via their keys. 224 | 225 | 226 | How to Use a Naming Strategy 227 | ---------------------------- 228 | 229 | `Official documentation 230 | `__ 231 | 232 | Laminas Configuration 233 | 234 | .. code:: php 235 | 236 | return [ 237 | 'service_manager' => [ 238 | 'invokables' => [ 239 | \Doctrine\ORM\Mapping\UnderscoreNamingStrategy::class => \Doctrine\ORM\Mapping\UnderscoreNamingStrategy::class, 240 | ], 241 | ], 242 | 'doctrine' => [ 243 | 'configuration' => [ 244 | 'orm_default' => [ 245 | 'naming_strategy' => \Doctrine\ORM\Mapping\UnderscoreNamingStrategy::class, 246 | ], 247 | ], 248 | ], 249 | ]; 250 | 251 | How to Use a Quote Strategy 252 | --------------------------- 253 | 254 | `Official 255 | documentation `__ 256 | 257 | Laminas Configuration 258 | 259 | .. code:: php 260 | 261 | return [ 262 | 'service_manager' => [ 263 | 'invokables' => [ 264 | \Doctrine\ORM\Mapping\AnsiQuoteStrategy::class => \Doctrine\ORM\Mapping\AnsiQuoteStrategy::class, 265 | ], 266 | ], 267 | 'doctrine' => [ 268 | 'configuration' => [ 269 | 'orm_default' => [ 270 | 'quote_strategy' => \Doctrine\ORM\Mapping\AnsiQuoteStrategy::class, 271 | ], 272 | ], 273 | ], 274 | ]; 275 | 276 | How to Override RunSqlCommand Creation 277 | -------------------------------------- 278 | 279 | The following Laminas configuration can be used to override the creation of the 280 | ``Doctrine\DBAL\Tools\Console\Command\RunSqlCommand`` instance used by this 281 | module. 282 | 283 | .. code:: php 284 | 285 | return [ 286 | 'service_manager' => [ 287 | 'factories' => [ 288 | 'doctrine.dbal_cmd.runsql' => MyCustomRunSqlCommandFactory::class, 289 | ], 290 | ], 291 | ]; 292 | 293 | How to Exclude Tables from a Schema Diff 294 | ---------------------------------------- 295 | 296 | The "schema_assets_filter" option can be used to exclude certain tables from being deleted in a schema update. 297 | It should be set with a filter callback that will receive the table name and should return `false` for any tables that must be excluded and `true` for any other tables. 298 | 299 | .. code:: php 300 | 301 | return [ 302 | 'doctrine' => [ 303 | 'configuration' => [ 304 | 'orm_default' => [ 305 | 'schema_assets_filter' => fn (string $tableName): bool => ( 306 | ! in_array($tableName, ['doNotRemoveThisTable', 'alsoDoNotRemoveThisTable']) 307 | ), 308 | ], 309 | ], 310 | ], 311 | ]; 312 | 313 | .. note:: 314 | 315 | If you want your application config to be cached, you should use a callable in terms of a static 316 | function (like `MyFilterClass::filter`) instead of a closure. 317 | -------------------------------------------------------------------------------- /src/Form/Annotation/DoctrineAnnotationListener.php: -------------------------------------------------------------------------------- 1 | listeners[] = $events->attach( 32 | EntityBasedFormBuilder::EVENT_CONFIGURE_FIELD, 33 | [$this, 'handleFilterField'], 34 | ); 35 | $this->listeners[] = $events->attach( 36 | EntityBasedFormBuilder::EVENT_CONFIGURE_FIELD, 37 | [$this, 'handleTypeField'], 38 | ); 39 | $this->listeners[] = $events->attach( 40 | EntityBasedFormBuilder::EVENT_CONFIGURE_FIELD, 41 | [$this, 'handleValidatorField'], 42 | ); 43 | $this->listeners[] = $events->attach( 44 | EntityBasedFormBuilder::EVENT_CONFIGURE_FIELD, 45 | [$this, 'handleRequiredField'], 46 | ); 47 | $this->listeners[] = $events->attach( 48 | EntityBasedFormBuilder::EVENT_EXCLUDE_FIELD, 49 | [$this, 'handleExcludeField'], 50 | ); 51 | $this->listeners[] = $events->attach( 52 | EntityBasedFormBuilder::EVENT_CONFIGURE_ASSOCIATION, 53 | [$this, 'handleToOne'], 54 | ); 55 | $this->listeners[] = $events->attach( 56 | EntityBasedFormBuilder::EVENT_CONFIGURE_ASSOCIATION, 57 | [$this, 'handleToMany'], 58 | ); 59 | $this->listeners[] = $events->attach( 60 | EntityBasedFormBuilder::EVENT_CONFIGURE_ASSOCIATION, 61 | [$this, 'handleRequiredAssociation'], 62 | ); 63 | $this->listeners[] = $events->attach( 64 | EntityBasedFormBuilder::EVENT_EXCLUDE_ASSOCIATION, 65 | [$this, 'handleExcludeAssociation'], 66 | ); 67 | } 68 | 69 | /** @internal */ 70 | public function handleToOne(EventInterface $event): void 71 | { 72 | $metadata = $event->getParam('metadata'); 73 | $mapping = $this->getAssociationMapping($event); 74 | if (! $mapping || ! $metadata->isSingleValuedAssociation($event->getParam('name'))) { 75 | return; 76 | } 77 | 78 | $this->prepareEvent($event); 79 | $this->mergeAssociationOptions($event->getParam('elementSpec'), $mapping['targetEntity']); 80 | } 81 | 82 | /** @internal */ 83 | public function handleToMany(EventInterface $event): void 84 | { 85 | $metadata = $event->getParam('metadata'); 86 | $mapping = $this->getAssociationMapping($event); 87 | if (! $mapping || ! $metadata->isCollectionValuedAssociation($event->getParam('name'))) { 88 | return; 89 | } 90 | 91 | $this->prepareEvent($event); 92 | 93 | $elementSpec = $event->getParam('elementSpec'); 94 | $inputSpec = $event->getParam('inputSpec'); 95 | $inputSpec['required'] = false; 96 | 97 | $this->mergeAssociationOptions($elementSpec, $mapping['targetEntity']); 98 | 99 | $elementSpec['spec']['attributes']['multiple'] = true; 100 | } 101 | 102 | /** @internal */ 103 | public function handleExcludeAssociation(EventInterface $event): bool 104 | { 105 | $metadata = $event->getParam('metadata'); 106 | 107 | return $metadata && $metadata->isAssociationInverseSide($event->getParam('name')); 108 | } 109 | 110 | /** @internal */ 111 | public function handleExcludeField(EventInterface $event): bool 112 | { 113 | $metadata = $event->getParam('metadata'); 114 | $identifiers = $metadata->getIdentifierFieldNames(); 115 | 116 | return in_array($event->getParam('name'), $identifiers) && 117 | $metadata->generatorType === ClassMetadata::GENERATOR_TYPE_IDENTITY; 118 | } 119 | 120 | /** @internal */ 121 | public function handleFilterField(EventInterface $event): void 122 | { 123 | $metadata = $event->getParam('metadata'); 124 | if (! $metadata || ! $metadata->hasField($event->getParam('name'))) { 125 | return; 126 | } 127 | 128 | $this->prepareEvent($event); 129 | 130 | $inputSpec = $event->getParam('inputSpec'); 131 | 132 | switch ($metadata->getTypeOfField($event->getParam('name'))) { 133 | case 'bool': 134 | case 'boolean': 135 | $inputSpec['filters'][] = ['name' => 'Boolean']; 136 | break; 137 | case 'bigint': 138 | case 'integer': 139 | case 'smallint': 140 | $inputSpec['filters'][] = ['name' => 'Int']; 141 | break; 142 | case 'datetime': 143 | case 'datetime_immutable': 144 | case 'datetimetz': 145 | case 'datetimetz_immutable': 146 | case 'date': 147 | case 'time': 148 | case 'string': 149 | case 'text': 150 | $inputSpec['filters'][] = ['name' => 'StringTrim']; 151 | break; 152 | } 153 | } 154 | 155 | /** @internal */ 156 | public function handleRequiredAssociation(EventInterface $event): void 157 | { 158 | $metadata = $event->getParam('metadata'); 159 | $mapping = $this->getAssociationMapping($event); 160 | if (! $mapping) { 161 | return; 162 | } 163 | 164 | $this->prepareEvent($event); 165 | 166 | $inputSpec = $event->getParam('inputSpec'); 167 | $elementSpec = $event->getParam('elementSpec'); 168 | 169 | if ($metadata->isCollectionValuedAssociation($event->getParam('name'))) { 170 | $inputSpec['required'] = false; 171 | } elseif (isset($mapping['joinColumns'])) { 172 | $required = true; 173 | foreach ($mapping['joinColumns'] as $joinColumn) { 174 | if (! isset($joinColumn['nullable']) || ! $joinColumn['nullable']) { 175 | continue; 176 | } 177 | 178 | $required = false; 179 | if ( 180 | (isset($elementSpec['spec']['options']) && 181 | ! array_key_exists('empty_option', $elementSpec['spec']['options'])) || 182 | ! isset($elementSpec['spec']['options']) 183 | ) { 184 | $elementSpec['spec']['options']['empty_option'] = 'NULL'; 185 | } 186 | 187 | break; 188 | } 189 | 190 | $inputSpec['required'] = $required; 191 | } 192 | } 193 | 194 | /** @internal */ 195 | public function handleRequiredField(EventInterface $event): void 196 | { 197 | $this->prepareEvent($event); 198 | 199 | $metadata = $event->getParam('metadata'); 200 | $inputSpec = $event->getParam('inputSpec'); 201 | 202 | if (! $metadata || ! $metadata->hasField($event->getParam('name'))) { 203 | return; 204 | } 205 | 206 | $inputSpec['required'] = ! $metadata->isNullable($event->getParam('name')); 207 | } 208 | 209 | /** @internal */ 210 | public function handleTypeField(EventInterface $event): void 211 | { 212 | $metadata = $event->getParam('metadata'); 213 | $mapping = $this->getFieldMapping($event); 214 | if (! $mapping) { 215 | return; 216 | } 217 | 218 | $this->prepareEvent($event); 219 | 220 | $elementSpec = $event->getParam('elementSpec'); 221 | 222 | if (isset($elementSpec['spec']['options']['target_class'])) { 223 | $this->mergeAssociationOptions($elementSpec, $elementSpec['spec']['options']['target_class']); 224 | 225 | return; 226 | } 227 | 228 | if (isset($elementSpec['spec']['type']) || isset($elementSpec['spec']['attributes']['type'])) { 229 | return; 230 | } 231 | 232 | switch ($metadata->getTypeOfField($event->getParam('name'))) { 233 | case 'bigint': 234 | case 'integer': 235 | case 'smallint': 236 | $type = LaminasFormElement\Number::class; 237 | break; 238 | case 'bool': 239 | case 'boolean': 240 | $type = LaminasFormElement\Checkbox::class; 241 | break; 242 | case 'date': 243 | $type = LaminasFormElement\Date::class; 244 | break; 245 | case 'datetime': 246 | case 'datetime_immutable': 247 | case 'datetimetz': 248 | case 'datetimetz_immutable': 249 | $type = LaminasFormElement\DateTimeLocal::class; 250 | break; 251 | case 'time': 252 | $type = LaminasFormElement\Time::class; 253 | break; 254 | case 'text': 255 | $type = LaminasFormElement\Textarea::class; 256 | break; 257 | default: 258 | $type = LaminasFormElement::class; 259 | break; 260 | } 261 | 262 | $elementSpec['spec']['type'] = $type; 263 | } 264 | 265 | /** @internal */ 266 | public function handleValidatorField(EventInterface $event): void 267 | { 268 | $mapping = $this->getFieldMapping($event); 269 | if (! $mapping) { 270 | return; 271 | } 272 | 273 | $metadata = $event->getParam('metadata'); 274 | 275 | $this->prepareEvent($event); 276 | 277 | $inputSpec = $event->getParam('inputSpec'); 278 | 279 | switch ($metadata->getTypeOfField($event->getParam('name'))) { 280 | case 'bool': 281 | case 'boolean': 282 | $inputSpec['validators'][] = [ 283 | 'name' => 'InArray', 284 | 'options' => ['haystack' => ['0', '1']], 285 | ]; 286 | break; 287 | case 'float': 288 | $inputSpec['validators'][] = ['name' => 'Float']; 289 | break; 290 | case 'bigint': 291 | case 'integer': 292 | case 'smallint': 293 | $inputSpec['validators'][] = ['name' => 'Int']; 294 | break; 295 | case 'string': 296 | $elementSpec = $event->getParam('elementSpec'); 297 | if ( 298 | isset($elementSpec['spec']['type']) && 299 | in_array($elementSpec['spec']['type'], ['File', LaminasFormElement\File::class]) 300 | ) { 301 | return; 302 | } 303 | 304 | if (isset($mapping['length'])) { 305 | $inputSpec['validators'][] = [ 306 | 'name' => 'StringLength', 307 | 'options' => ['max' => $mapping['length']], 308 | ]; 309 | } 310 | 311 | break; 312 | } 313 | } 314 | 315 | /** @return mixed[]|null */ 316 | protected function getFieldMapping(EventInterface $event): array|null 317 | { 318 | $metadata = $event->getParam('metadata'); 319 | if ($metadata && $metadata->hasField($event->getParam('name'))) { 320 | return $metadata->getFieldMapping($event->getParam('name')); 321 | } 322 | 323 | return null; 324 | } 325 | 326 | /** @return mixed[]|null */ 327 | protected function getAssociationMapping(EventInterface $event): array|null 328 | { 329 | $metadata = $event->getParam('metadata'); 330 | if ($metadata && $metadata->hasAssociation($event->getParam('name'))) { 331 | return $metadata->getAssociationMapping($event->getParam('name')); 332 | } 333 | 334 | return null; 335 | } 336 | 337 | protected function mergeAssociationOptions(ArrayObject $elementSpec, string $targetEntity): void 338 | { 339 | $options = $elementSpec['spec']['options'] ?? []; 340 | $options = array_merge( 341 | [ 342 | 'object_manager' => $this->objectManager, 343 | 'target_class' => $targetEntity, 344 | ], 345 | $options, 346 | ); 347 | 348 | $elementSpec['spec']['options'] = $options; 349 | if (isset($elementSpec['spec']['type'])) { 350 | return; 351 | } 352 | 353 | $elementSpec['spec']['type'] = EntitySelect::class; 354 | } 355 | 356 | /** 357 | * Normalizes event setting all expected parameters. 358 | */ 359 | protected function prepareEvent(EventInterface $event): void 360 | { 361 | foreach (['elementSpec', 'inputSpec'] as $type) { 362 | if ($event->getParam($type)) { 363 | continue; 364 | } 365 | 366 | $event->setParam($type, new ArrayObject()); 367 | } 368 | 369 | $elementSpec = $event->getParam('elementSpec'); 370 | $inputSpec = $event->getParam('inputSpec'); 371 | 372 | if (! isset($elementSpec['spec'])) { 373 | $elementSpec['spec'] = []; 374 | } 375 | 376 | if (! isset($inputSpec['filters'])) { 377 | $inputSpec['filters'] = []; 378 | } 379 | 380 | if (isset($inputSpec['validators'])) { 381 | return; 382 | } 383 | 384 | $inputSpec['validators'] = []; 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /config/module.config.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'connection' => [ 15 | // Configuration for service `doctrine.connection.orm_default` service 16 | 'orm_default' => [ 17 | // configuration instance to use. The retrieved service name will 18 | // be `doctrine.configuration.$thisSetting` 19 | 'configuration' => 'orm_default', 20 | 21 | // event manager instance to use. The retrieved service name will 22 | // be `doctrine.eventmanager.$thisSetting` 23 | 'eventmanager' => 'orm_default', 24 | 25 | // connection parameters, see 26 | // http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html 27 | 'params' => [ 28 | 'host' => 'localhost', 29 | 'port' => '3306', 30 | 'user' => 'username', 31 | 'password' => 'password', 32 | 'dbname' => 'database', 33 | ], 34 | ], 35 | ], 36 | 37 | // Configuration details for the ORM. 38 | // See http://docs.doctrine-project.org/en/latest/reference/configuration.html 39 | 'configuration' => [ 40 | // Configuration for service `doctrine.configuration.orm_default` service 41 | 'orm_default' => [ 42 | // metadata cache instance to use. The retrieved service name will 43 | // be `doctrine.cache.$thisSetting` 44 | 'metadata_cache' => 'array', 45 | 46 | // DQL queries parsing cache instance to use. The retrieved service 47 | // name will be `doctrine.cache.$thisSetting` 48 | 'query_cache' => 'array', 49 | 50 | // ResultSet cache to use. The retrieved service name will be 51 | // `doctrine.cache.$thisSetting` 52 | 'result_cache' => 'array', 53 | 54 | // Hydration cache to use. The retrieved service name will be 55 | // `doctrine.cache.$thisSetting` 56 | 'hydration_cache' => 'array', 57 | 58 | // Mapping driver instance to use. Change this only if you don't want 59 | // to use the default chained driver. The retrieved service name will 60 | // be `doctrine.driver.$thisSetting` 61 | 'driver' => 'orm_default', 62 | 63 | // Generate proxies automatically (turn off for production) 64 | 'generate_proxies' => true, 65 | 66 | // directory where proxies will be stored. By default, this is in 67 | // the `data` directory of your application 68 | 'proxy_dir' => 'data/DoctrineORMModule/Proxy', 69 | 70 | // namespace for generated proxy classes 71 | 'proxy_namespace' => 'DoctrineORMModule\Proxy', 72 | 73 | // SQL filters. See http://docs.doctrine-project.org/en/latest/reference/filters.html 74 | 'filters' => [], 75 | 76 | // Custom DQL functions. 77 | // You can grab common MySQL ones at https://github.com/beberlei/DoctrineExtensions 78 | // Further docs at http://docs.doctrine-project.org/en/latest/cookbook/dql-user-defined-functions.html 79 | 'datetime_functions' => [], 80 | 'string_functions' => [], 81 | 'numeric_functions' => [], 82 | 83 | // Second level cache configuration (see doc to learn about configuration) 84 | 'second_level_cache' => [], 85 | ], 86 | ], 87 | 88 | // Metadata Mapping driver configuration 89 | 'driver' => [ 90 | // Configuration for service `doctrine.driver.orm_default` service 91 | 'orm_default' => [ 92 | // By default, the ORM module uses a driver chain. This allows multiple 93 | // modules to define their own entities 94 | 'class' => MappingDriverChain::class, 95 | 96 | // Map of driver names to be used within this driver chain, indexed by 97 | // entity namespace 98 | 'drivers' => [], 99 | ], 100 | ], 101 | 102 | // Entity Manager instantiation settings 103 | 'entitymanager' => [ 104 | // configuration for the `doctrine.entitymanager.orm_default` service 105 | 'orm_default' => [ 106 | // connection instance to use. The retrieved service name will 107 | // be `doctrine.connection.$thisSetting` 108 | 'connection' => 'orm_default', 109 | 110 | // configuration instance to use. The retrieved service name will 111 | // be `doctrine.configuration.$thisSetting` 112 | 'configuration' => 'orm_default', 113 | ], 114 | ], 115 | 116 | 'eventmanager' => [ 117 | // configuration for the `doctrine.eventmanager.orm_default` service 118 | 'orm_default' => [], 119 | ], 120 | 121 | // SQL logger collector, used when Laminas\DeveloperTools and its toolbar are active 122 | 'sql_logger_collector' => [ 123 | // configuration for the `doctrine.sql_logger_collector.orm_default` service 124 | 'orm_default' => [], 125 | ], 126 | 127 | // mappings collector, used when Laminas\DeveloperTools and its toolbar are active 128 | 'mapping_collector' => [ 129 | // configuration for the `doctrine.sql_logger_collector.orm_default` service 130 | 'orm_default' => [], 131 | ], 132 | 133 | // entity resolver configuration, allows mapping associations to interfaces 134 | 'entity_resolver' => [ 135 | // configuration for the `doctrine.entity_resolver.orm_default` service 136 | 'orm_default' => [], 137 | ], 138 | 139 | // authentication service configuration 140 | 'authentication' => [ 141 | // configuration for the `doctrine.authentication.orm_default` authentication service 142 | 'orm_default' => [ 143 | // name of the object manager to use. By default, the EntityManager is used 144 | 'objectManager' => 'doctrine.entitymanager.orm_default', 145 | //'identityClass' => 'Application\Model\User', 146 | //'identityProperty' => 'username', 147 | //'credentialProperty' => 'password', 148 | ], 149 | ], 150 | 151 | // migrations configuration 152 | 'migrations_configuration' => [ 153 | 'orm_default' => [ 154 | 'table_storage' => [ 155 | 'table_name' => 'DoctrineMigrationVersions', 156 | 'version_column_name' => 'version', 157 | 'version_column_length' => 191, 158 | 'executed_at_column_name' => 'executedAt', 159 | 'execution_time_column_name' => 'executionTime', 160 | ], 161 | 'migrations_paths' => [], 162 | 'migrations' => [], 163 | 'all_or_nothing' => false, 164 | 'check_database_platform' => true, 165 | // 'organize_migrations' => 'year', // year or year_and_month or leave unchanged for default 166 | 'custom_template' => null, 167 | 'dependency_factory_services' => [], 168 | ], 169 | ], 170 | 171 | // migrations commands base config 172 | 'migrations_cmd' => [ 173 | 'current' => [], 174 | 'dumpschema' => [], 175 | 'diff' => [], 176 | 'generate' => [], 177 | 'execute' => [], 178 | 'latest' => [], 179 | 'list' => [], 180 | 'migrate' => [], 181 | 'rollup' => [], 182 | 'status' => [], 183 | 'syncmetadatastorage' => [], 184 | 'uptodate' => [], 185 | 'version' => [], 186 | ], 187 | ], 188 | 'service_manager' => [ 189 | 'factories' => [ 190 | CliConfigurator::class => Service\CliConfiguratorFactory::class, 191 | 'Doctrine\ORM\EntityManager' => Service\EntityManagerAliasCompatFactory::class, 192 | // DBAL commands 193 | 'doctrine.dbal_cmd.runsql' => Service\RunSqlCommandFactory::class, 194 | 'doctrine.dbal_cmd.reserved_words' => Service\ReservedWordsCommandFactory::class, 195 | ], 196 | 'invokables' => [ 197 | // ORM Commands 198 | 'doctrine.orm_cmd.clear_cache_metadata' => Command\ClearCache\MetadataCommand::class, 199 | 'doctrine.orm_cmd.clear_cache_result' => Command\ClearCache\ResultCommand::class, 200 | 'doctrine.orm_cmd.clear_cache_query' => Command\ClearCache\QueryCommand::class, 201 | 'doctrine.orm_cmd.schema_tool_create' => Command\SchemaTool\CreateCommand::class, 202 | 'doctrine.orm_cmd.schema_tool_update' => Command\SchemaTool\UpdateCommand::class, 203 | 'doctrine.orm_cmd.schema_tool_drop' => Command\SchemaTool\DropCommand::class, 204 | 'doctrine.orm_cmd.convert_d1_schema' => Command\ConvertDoctrine1SchemaCommand::class, 205 | 'doctrine.orm_cmd.generate_entities' => Command\GenerateEntitiesCommand::class, 206 | 'doctrine.orm_cmd.generate_proxies' => Command\GenerateProxiesCommand::class, 207 | 'doctrine.orm_cmd.convert_mapping' => Command\ConvertMappingCommand::class, 208 | 'doctrine.orm_cmd.run_dql' => Command\RunDqlCommand::class, 209 | 'doctrine.orm_cmd.validate_schema' => Command\ValidateSchemaCommand::class, 210 | 'doctrine.orm_cmd.info' => Command\InfoCommand::class, 211 | 'doctrine.orm_cmd.ensure_production_settings' => Command\EnsureProductionSettingsCommand::class, 212 | 'doctrine.orm_cmd.generate_repositories' => Command\GenerateRepositoriesCommand::class, 213 | ], 214 | ], 215 | 216 | // Factory mappings - used to define which factory to use to instantiate a particular doctrine 217 | // service type 218 | 'doctrine_factories' => [ 219 | 'connection' => Service\DBALConnectionFactory::class, 220 | 'configuration' => Service\ConfigurationFactory::class, 221 | 'entitymanager' => Service\EntityManagerFactory::class, 222 | 'entity_resolver' => Service\EntityResolverFactory::class, 223 | 'sql_logger_collector' => Service\SQLLoggerCollectorFactory::class, 224 | 'mapping_collector' => Service\MappingCollectorFactory::class, 225 | 'migrations_cmd' => Service\MigrationsCommandFactory::class, 226 | ], 227 | 228 | // Laminas\Form\FormElementManager configuration 229 | 'form_elements' => [ 230 | 'aliases' => [ 231 | 'objectselect' => Element\ObjectSelect::class, 232 | 'objectradio' => Element\ObjectRadio::class, 233 | 'objectmulticheckbox' => Element\ObjectMultiCheckbox::class, 234 | ], 235 | 'factories' => [ 236 | Element\ObjectSelect::class => Service\ObjectSelectFactory::class, 237 | Element\ObjectRadio::class => Service\ObjectRadioFactory::class, 238 | Element\ObjectMultiCheckbox::class => Service\ObjectMultiCheckboxFactory::class, 239 | ], 240 | ], 241 | 242 | 'hydrators' => [ 243 | 'factories' => [ 244 | DoctrineObject::class => Service\DoctrineObjectHydratorFactory::class, 245 | ], 246 | ], 247 | 248 | //////////////////////////////////////////////////////////////////// 249 | // `laminas/laminas-developer-tools` specific settings // 250 | // ignore these if you're not developing additional features for // 251 | // laminas developer tools // 252 | //////////////////////////////////////////////////////////////////// 253 | 254 | 'router' => [ 255 | 'routes' => [ 256 | 'doctrine_orm_module_yuml' => [ 257 | 'type' => 'literal', 258 | 'options' => [ 259 | 'route' => '/ocra_service_manager_yuml', 260 | 'defaults' => [ 261 | 'controller' => Yuml\YumlController::class, 262 | 'action' => 'index', 263 | ], 264 | ], 265 | ], 266 | ], 267 | ], 268 | 269 | 'view_manager' => [ 270 | 'template_map' => [ 271 | 'laminas-developer-tools/toolbar/doctrine-orm-queries' 272 | => __DIR__ . '/../view/laminas-developer-tools/toolbar/doctrine-orm-queries.phtml', 273 | 'laminas-developer-tools/toolbar/doctrine-orm-mappings' 274 | => __DIR__ . '/../view/laminas-developer-tools/toolbar/doctrine-orm-mappings.phtml', 275 | ], 276 | ], 277 | 278 | 'laminas-developer-tools' => [ 279 | 'profiler' => [ 280 | 'collectors' => [ 281 | 'doctrine.sql_logger_collector.orm_default' => 'doctrine.sql_logger_collector.orm_default', 282 | 'doctrine.mapping_collector.orm_default' => 'doctrine.mapping_collector.orm_default', 283 | ], 284 | ], 285 | 'toolbar' => [ 286 | 'entries' => [ 287 | 'doctrine.sql_logger_collector.orm_default' => 'laminas-developer-tools/toolbar/doctrine-orm-queries', 288 | 'doctrine.mapping_collector.orm_default' => 'laminas-developer-tools/toolbar/doctrine-orm-mappings', 289 | ], 290 | ], 291 | ], 292 | ]; 293 | 294 | if (class_exists(ImportCommand::class)) { 295 | $result['service_manager']['invokables']['doctrine.dbal_cmd.import'] = ImportCommand::class; 296 | } 297 | 298 | return $result; 299 | -------------------------------------------------------------------------------- /src/Options/Configuration.php: -------------------------------------------------------------------------------- 1 | > 183 | */ 184 | protected array $middlewares = []; 185 | 186 | /** 187 | * Configuration option for the schema assets filter callable 188 | * 189 | * @var callable|null 190 | */ 191 | protected $schemaAssetsFilter = null; 192 | 193 | /** @param mixed[] $datetimeFunctions */ 194 | public function setDatetimeFunctions(array $datetimeFunctions): self 195 | { 196 | $this->datetimeFunctions = $datetimeFunctions; 197 | 198 | return $this; 199 | } 200 | 201 | /** @return mixed[] */ 202 | public function getDatetimeFunctions(): array 203 | { 204 | return $this->datetimeFunctions; 205 | } 206 | 207 | public function setDriver(string $driver): self 208 | { 209 | $this->driver = $driver; 210 | 211 | return $this; 212 | } 213 | 214 | public function getDriver(): string 215 | { 216 | return 'doctrine.driver.' . $this->driver; 217 | } 218 | 219 | /** @param mixed[] $entityNamespaces */ 220 | public function setEntityNamespaces(array $entityNamespaces): self 221 | { 222 | $this->entityNamespaces = $entityNamespaces; 223 | 224 | return $this; 225 | } 226 | 227 | /** @return mixed[] */ 228 | public function getEntityNamespaces(): array 229 | { 230 | return $this->entityNamespaces; 231 | } 232 | 233 | public function setGenerateProxies(bool $generateProxies): self 234 | { 235 | $this->generateProxies = $generateProxies; 236 | 237 | return $this; 238 | } 239 | 240 | public function getGenerateProxies(): bool 241 | { 242 | return $this->generateProxies; 243 | } 244 | 245 | public function setMetadataCache(string $metadataCache): self 246 | { 247 | $this->metadataCache = $metadataCache; 248 | 249 | return $this; 250 | } 251 | 252 | public function getMetadataCache(): string 253 | { 254 | return 'doctrine.cache.' . $this->metadataCache; 255 | } 256 | 257 | public function setResultCache(string $resultCache): void 258 | { 259 | $this->resultCache = $resultCache; 260 | } 261 | 262 | public function getResultCache(): string 263 | { 264 | return 'doctrine.cache.' . $this->resultCache; 265 | } 266 | 267 | public function setHydrationCache(string $hydrationCache): self 268 | { 269 | $this->hydrationCache = $hydrationCache; 270 | 271 | return $this; 272 | } 273 | 274 | public function getHydrationCache(): string 275 | { 276 | return 'doctrine.cache.' . $this->hydrationCache; 277 | } 278 | 279 | /** @param mixed[] $namedNativeQueries */ 280 | public function setNamedNativeQueries(array $namedNativeQueries): self 281 | { 282 | $this->namedNativeQueries = $namedNativeQueries; 283 | 284 | return $this; 285 | } 286 | 287 | /** @return mixed[] */ 288 | public function getNamedNativeQueries(): array 289 | { 290 | return $this->namedNativeQueries; 291 | } 292 | 293 | /** @param mixed[] $namedQueries */ 294 | public function setNamedQueries(array $namedQueries): self 295 | { 296 | $this->namedQueries = $namedQueries; 297 | 298 | return $this; 299 | } 300 | 301 | /** @return mixed[] */ 302 | public function getNamedQueries(): array 303 | { 304 | return $this->namedQueries; 305 | } 306 | 307 | /** @param mixed[] $numericFunctions */ 308 | public function setNumericFunctions(array $numericFunctions): self 309 | { 310 | $this->numericFunctions = $numericFunctions; 311 | 312 | return $this; 313 | } 314 | 315 | /** @return mixed[] */ 316 | public function getNumericFunctions(): array 317 | { 318 | return $this->numericFunctions; 319 | } 320 | 321 | /** @param mixed[] $filters */ 322 | public function setFilters(array $filters): self 323 | { 324 | $this->filters = $filters; 325 | 326 | return $this; 327 | } 328 | 329 | /** @return mixed[] */ 330 | public function getFilters(): array 331 | { 332 | return $this->filters; 333 | } 334 | 335 | public function setProxyDir(string $proxyDir): self 336 | { 337 | $this->proxyDir = $proxyDir; 338 | 339 | return $this; 340 | } 341 | 342 | public function getProxyDir(): string 343 | { 344 | return $this->proxyDir; 345 | } 346 | 347 | public function setProxyNamespace(string $proxyNamespace): self 348 | { 349 | $this->proxyNamespace = $proxyNamespace; 350 | 351 | return $this; 352 | } 353 | 354 | public function getProxyNamespace(): string 355 | { 356 | return $this->proxyNamespace; 357 | } 358 | 359 | public function setQueryCache(string $queryCache): self 360 | { 361 | $this->queryCache = $queryCache; 362 | 363 | return $this; 364 | } 365 | 366 | public function getQueryCache(): string 367 | { 368 | return 'doctrine.cache.' . $this->queryCache; 369 | } 370 | 371 | /** @param mixed[] $stringFunctions */ 372 | public function setStringFunctions(array $stringFunctions): self 373 | { 374 | $this->stringFunctions = $stringFunctions; 375 | 376 | return $this; 377 | } 378 | 379 | /** @return mixed[] */ 380 | public function getStringFunctions(): array 381 | { 382 | return $this->stringFunctions; 383 | } 384 | 385 | /** @param mixed[] $modes */ 386 | public function setCustomHydrationModes(array $modes): self 387 | { 388 | $this->customHydrationModes = $modes; 389 | 390 | return $this; 391 | } 392 | 393 | /** @return mixed[] */ 394 | public function getCustomHydrationModes(): array 395 | { 396 | return $this->customHydrationModes; 397 | } 398 | 399 | public function setNamingStrategy(string|NamingStrategy|null $namingStrategy): self 400 | { 401 | $this->namingStrategy = $namingStrategy; 402 | 403 | return $this; 404 | } 405 | 406 | public function getNamingStrategy(): string|NamingStrategy|null 407 | { 408 | return $this->namingStrategy; 409 | } 410 | 411 | public function setQuoteStrategy(string|QuoteStrategy|null $quoteStrategy): self 412 | { 413 | $this->quoteStrategy = $quoteStrategy; 414 | 415 | return $this; 416 | } 417 | 418 | public function getQuoteStrategy(): string|QuoteStrategy|null 419 | { 420 | return $this->quoteStrategy; 421 | } 422 | 423 | public function setRepositoryFactory(string|RepositoryFactory|null $repositoryFactory): self 424 | { 425 | $this->repositoryFactory = $repositoryFactory; 426 | 427 | return $this; 428 | } 429 | 430 | public function getRepositoryFactory(): string|RepositoryFactory|null 431 | { 432 | return $this->repositoryFactory; 433 | } 434 | 435 | /** 436 | * Set the metadata factory class name to use 437 | * 438 | * @see \Doctrine\ORM\Configuration::setClassMetadataFactoryName() 439 | */ 440 | public function setClassMetadataFactoryName(string $factoryName): self 441 | { 442 | $this->classMetadataFactoryName = $factoryName; 443 | 444 | return $this; 445 | } 446 | 447 | public function getClassMetadataFactoryName(): string|null 448 | { 449 | return $this->classMetadataFactoryName; 450 | } 451 | 452 | public function setEntityListenerResolver(string|EntityListenerResolver|null $entityListenerResolver): self 453 | { 454 | $this->entityListenerResolver = $entityListenerResolver; 455 | 456 | return $this; 457 | } 458 | 459 | public function getEntityListenerResolver(): string|EntityListenerResolver|null 460 | { 461 | return $this->entityListenerResolver; 462 | } 463 | 464 | /** @param mixed[] $secondLevelCache */ 465 | public function setSecondLevelCache(array $secondLevelCache): self 466 | { 467 | $this->secondLevelCache = new SecondLevelCacheConfiguration($secondLevelCache); 468 | 469 | return $this; 470 | } 471 | 472 | public function getSecondLevelCache(): SecondLevelCacheConfiguration 473 | { 474 | return $this->secondLevelCache ?: new SecondLevelCacheConfiguration(); 475 | } 476 | 477 | public function setFilterSchemaAssetsExpression(string $filterSchemaAssetsExpression): self 478 | { 479 | $this->filterSchemaAssetsExpression = $filterSchemaAssetsExpression; 480 | 481 | return $this; 482 | } 483 | 484 | public function getFilterSchemaAssetsExpression(): string|null 485 | { 486 | return $this->filterSchemaAssetsExpression; 487 | } 488 | 489 | public function setSchemaAssetsFilter(callable $schemaAssetsFilter): self 490 | { 491 | $this->schemaAssetsFilter = $schemaAssetsFilter; 492 | 493 | return $this; 494 | } 495 | 496 | public function getSchemaAssetsFilter(): callable|null 497 | { 498 | return $this->schemaAssetsFilter; 499 | } 500 | 501 | /** 502 | * Sets default repository class. 503 | */ 504 | public function setDefaultRepositoryClassName(string $className): self 505 | { 506 | $this->defaultRepositoryClassName = $className; 507 | 508 | return $this; 509 | } 510 | 511 | /** 512 | * Get default repository class name. 513 | */ 514 | public function getDefaultRepositoryClassName(): string|null 515 | { 516 | return $this->defaultRepositoryClassName; 517 | } 518 | 519 | /** @param array> $middlewares */ 520 | public function setMiddlewares(array $middlewares): void 521 | { 522 | $this->middlewares = $middlewares; 523 | } 524 | 525 | /** @return array> */ 526 | public function getMiddlewares(): array 527 | { 528 | return $this->middlewares; 529 | } 530 | } 531 | --------------------------------------------------------------------------------